From 957c774b622d06b29c4c59e48db59e2e7f2ae734 Mon Sep 17 00:00:00 2001 From: Tor Norbye <tnorbye@google.com> Date: Fri, 26 Jan 2018 17:12:10 -0800 Subject: [PATCH] Initial version of Metalava. For full information, read README.md. Test: ./gradlew test Change-Id: Ibe7cf1162af36f50afa025c3af0ea9cdaa5135c7 --- .gitignore | 7 + .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/compiler.xml | 9 + .idea/copyright/aosp.xml | 6 + .idea/copyright/profiles_settings.xml | 3 + .idea/gradle.xml | 17 + .idea/misc.xml | 6 + .idea/vcs.xml | 6 + README.md | 439 +++ build.gradle | 96 + gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + manual/android/accounts/annotations.xml | 159 + manual/android/content/annotations.xml | 14 + manual/android/location/annotations.xml | 15 + manual/android/provider/annotations.xml | 89 + .../support/design/widget/annotations.xml | 9 + manual/android/text/annotations.xml | 14 + .../tools/lint/annotations/SdkUtils2.java | 107 + .../lint/checks/infrastructure/ClassName.kt | 171 + .../tools/metalava/AnnotationStatistics.kt | 429 +++ .../tools/metalava/AnnotationsMerger.kt | 734 +++++ .../com/android/tools/metalava/ApiAnalyzer.kt | 1085 +++++++ .../tools/metalava/ComparisonVisitor.kt | 300 ++ .../android/tools/metalava/Compatibility.kt | 162 + .../tools/metalava/CompatibilityCheck.kt | 174 ++ .../android/tools/metalava/DexApiWriter.kt | 73 + .../com/android/tools/metalava/DocAnalyzer.kt | 521 ++++ .../java/com/android/tools/metalava/Driver.kt | 686 ++++ .../tools/metalava/ExtractAnnotations.kt | 51 + .../tools/metalava/NullnessMigration.kt | 170 + .../com/android/tools/metalava/Options.kt | 1116 +++++++ .../android/tools/metalava/PackageFilter.kt | 26 + .../android/tools/metalava/ProguardWriter.kt | 134 + .../com/android/tools/metalava/Reporter.kt | 279 ++ .../android/tools/metalava/SignatureWriter.kt | 289 ++ .../com/android/tools/metalava/StubWriter.kt | 516 +++ .../com/android/tools/metalava/Terminal.kt | 95 + .../metalava/apilevels/AndroidJarReader.java | 166 + .../android/tools/metalava/apilevels/Api.java | 86 + .../tools/metalava/apilevels/ApiClass.java | 205 ++ .../tools/metalava/apilevels/ApiElement.java | 229 ++ .../metalava/apilevels/ApiGenerator.java | 212 ++ .../tools/metalava/doclava1/ApiFile.java | 877 ++++++ .../tools/metalava/doclava1/ApiInfo.kt | 264 ++ .../metalava/doclava1/ApiParseException.java | 49 + .../tools/metalava/doclava1/ApiPredicate.kt | 103 + .../metalava/doclava1/ElidingPredicate.kt | 31 + .../tools/metalava/doclava1/Errors.java | 236 ++ .../metalava/doclava1/FilterPredicate.kt | 35 + .../metalava/doclava1/SourcePositionInfo.java | 66 + .../tools/metalava/model/AnnotationItem.kt | 456 +++ .../android/tools/metalava/model/ClassItem.kt | 677 ++++ .../android/tools/metalava/model/Codebase.kt | 178 ++ .../tools/metalava/model/CompilationUnit.kt | 35 + .../tools/metalava/model/ConstructorItem.kt | 30 + .../android/tools/metalava/model/FieldItem.kt | 304 ++ .../com/android/tools/metalava/model/Item.kt | 223 ++ .../tools/metalava/model/MemberItem.kt | 30 + .../tools/metalava/model/MethodItem.kt | 383 +++ .../tools/metalava/model/ModifierList.kt | 294 ++ .../metalava/model/MutableModifierList.kt | 42 + .../tools/metalava/model/PackageItem.kt | 111 + .../tools/metalava/model/PackageList.kt | 44 + .../tools/metalava/model/ParameterItem.kt | 80 + .../android/tools/metalava/model/TypeItem.kt | 266 ++ .../tools/metalava/model/psi/ClassType.kt | 37 + .../tools/metalava/model/psi/Javadoc.kt | 219 ++ .../metalava/model/psi/PsiAnnotationItem.kt | 381 +++ .../metalava/model/psi/PsiBasedCodebase.kt | 974 ++++++ .../tools/metalava/model/psi/PsiClassItem.kt | 764 +++++ .../metalava/model/psi/PsiConstructorItem.kt | 249 ++ .../tools/metalava/model/psi/PsiFieldItem.kt | 137 + .../tools/metalava/model/psi/PsiItem.kt | 340 ++ .../tools/metalava/model/psi/PsiMethodItem.kt | 294 ++ .../metalava/model/psi/PsiModifierItem.kt | 298 ++ .../metalava/model/psi/PsiPackageItem.kt | 163 + .../metalava/model/psi/PsiParameterItem.kt | 123 + .../tools/metalava/model/psi/PsiTypeItem.kt | 756 +++++ .../model/text/TextBackedAnnotationItem.kt | 59 + .../metalava/model/text/TextClassItem.kt | 254 ++ .../model/text/TextConstructorItem.kt | 49 + .../metalava/model/text/TextFieldItem.kt | 79 + .../tools/metalava/model/text/TextItem.kt | 47 + .../metalava/model/text/TextMemberItem.kt | 35 + .../metalava/model/text/TextMethodItem.kt | 151 + .../metalava/model/text/TextModifiers.kt | 167 + .../metalava/model/text/TextPackageItem.kt | 59 + .../metalava/model/text/TextParameterItem.kt | 62 + .../tools/metalava/model/text/TextTypeItem.kt | 177 ++ .../metalava/model/visitors/ApiVisitor.kt | 99 + .../metalava/model/visitors/ItemVisitor.kt | 78 + .../model/visitors/PredicateVisitor.kt | 40 + .../metalava/model/visitors/TypeVisitor.kt | 26 + .../model/visitors/VisibleItemVisitor.kt | 39 + .../metalava/AnnotationStatisticsTest.kt | 163 + .../tools/metalava/AnnotationsMergerTest.kt | 127 + .../com/android/tools/metalava/ApiFileTest.kt | 2091 +++++++++++++ .../android/tools/metalava/ApiFromTextTest.kt | 280 ++ .../tools/metalava/CompatibilityCheckTest.kt | 377 +++ .../android/tools/metalava/DocAnalyzerTest.kt | 1030 ++++++ .../com/android/tools/metalava/DriverTest.kt | 1160 +++++++ .../tools/metalava/ExtractAnnotationsTest.kt | 432 +++ .../android/tools/metalava/KeepFileTest.kt | 84 + .../tools/metalava/NullnessMigrationTest.kt | 291 ++ .../com/android/tools/metalava/OptionsTest.kt | 207 ++ .../tools/metalava/PackageFilterTest.kt | 32 + .../tools/metalava/ShowAnnotationTest.kt | 127 + .../com/android/tools/metalava/StubsTest.kt | 2776 +++++++++++++++++ .../tools/metalava/SystemServiceCheckTest.kt | 365 +++ .../metalava/apilevels/ApiGeneratorTest.kt | 81 + .../model/TextBackedAnnotationItemTest.kt | 93 + .../tools/metalava/model/TypeItemTest.kt | 35 + .../metalava/model/text/TextTypeItemTest.kt | 54 + stub-annotations/build.gradle | 4 + .../android/support/annotation/Migrate.java | 29 + .../support/annotation/NewlyNonNull.java | 35 + .../support/annotation/NewlyNullable.java | 35 + .../support/annotation/RecentlyNonNull.java | 35 + .../support/annotation/RecentlyNullable.java | 35 + 123 files changed, 30123 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/aosp.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 manual/android/accounts/annotations.xml create mode 100644 manual/android/content/annotations.xml create mode 100644 manual/android/location/annotations.xml create mode 100644 manual/android/provider/annotations.xml create mode 100644 manual/android/support/design/widget/annotations.xml create mode 100644 manual/android/text/annotations.xml create mode 100644 src/main/java/com/android/tools/lint/annotations/SdkUtils2.java create mode 100644 src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt create mode 100644 src/main/java/com/android/tools/metalava/AnnotationStatistics.kt create mode 100644 src/main/java/com/android/tools/metalava/AnnotationsMerger.kt create mode 100644 src/main/java/com/android/tools/metalava/ApiAnalyzer.kt create mode 100644 src/main/java/com/android/tools/metalava/ComparisonVisitor.kt create mode 100644 src/main/java/com/android/tools/metalava/Compatibility.kt create mode 100644 src/main/java/com/android/tools/metalava/CompatibilityCheck.kt create mode 100644 src/main/java/com/android/tools/metalava/DexApiWriter.kt create mode 100644 src/main/java/com/android/tools/metalava/DocAnalyzer.kt create mode 100644 src/main/java/com/android/tools/metalava/Driver.kt create mode 100644 src/main/java/com/android/tools/metalava/ExtractAnnotations.kt create mode 100644 src/main/java/com/android/tools/metalava/NullnessMigration.kt create mode 100644 src/main/java/com/android/tools/metalava/Options.kt create mode 100644 src/main/java/com/android/tools/metalava/PackageFilter.kt create mode 100644 src/main/java/com/android/tools/metalava/ProguardWriter.kt create mode 100644 src/main/java/com/android/tools/metalava/Reporter.kt create mode 100644 src/main/java/com/android/tools/metalava/SignatureWriter.kt create mode 100644 src/main/java/com/android/tools/metalava/StubWriter.kt create mode 100644 src/main/java/com/android/tools/metalava/Terminal.kt create mode 100644 src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java create mode 100644 src/main/java/com/android/tools/metalava/apilevels/Api.java create mode 100644 src/main/java/com/android/tools/metalava/apilevels/ApiClass.java create mode 100644 src/main/java/com/android/tools/metalava/apilevels/ApiElement.java create mode 100644 src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java create mode 100644 src/main/java/com/android/tools/metalava/doclava1/ApiFile.java create mode 100644 src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt create mode 100644 src/main/java/com/android/tools/metalava/doclava1/ApiParseException.java create mode 100644 src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt create mode 100644 src/main/java/com/android/tools/metalava/doclava1/ElidingPredicate.kt create mode 100644 src/main/java/com/android/tools/metalava/doclava1/Errors.java create mode 100644 src/main/java/com/android/tools/metalava/doclava1/FilterPredicate.kt create mode 100644 src/main/java/com/android/tools/metalava/doclava1/SourcePositionInfo.java create mode 100644 src/main/java/com/android/tools/metalava/model/AnnotationItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/ClassItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/Codebase.kt create mode 100644 src/main/java/com/android/tools/metalava/model/CompilationUnit.kt create mode 100644 src/main/java/com/android/tools/metalava/model/ConstructorItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/FieldItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/Item.kt create mode 100644 src/main/java/com/android/tools/metalava/model/MemberItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/MethodItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/ModifierList.kt create mode 100644 src/main/java/com/android/tools/metalava/model/MutableModifierList.kt create mode 100644 src/main/java/com/android/tools/metalava/model/PackageItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/PackageList.kt create mode 100644 src/main/java/com/android/tools/metalava/model/ParameterItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/TypeItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/ClassType.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/Javadoc.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiConstructorItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiFieldItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiPackageItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt create mode 100644 src/main/java/com/android/tools/metalava/model/visitors/ApiVisitor.kt create mode 100644 src/main/java/com/android/tools/metalava/model/visitors/ItemVisitor.kt create mode 100644 src/main/java/com/android/tools/metalava/model/visitors/PredicateVisitor.kt create mode 100644 src/main/java/com/android/tools/metalava/model/visitors/TypeVisitor.kt create mode 100644 src/main/java/com/android/tools/metalava/model/visitors/VisibleItemVisitor.kt create mode 100644 src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt create mode 100644 src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt create mode 100644 src/test/java/com/android/tools/metalava/ApiFileTest.kt create mode 100644 src/test/java/com/android/tools/metalava/ApiFromTextTest.kt create mode 100644 src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt create mode 100644 src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt create mode 100644 src/test/java/com/android/tools/metalava/DriverTest.kt create mode 100644 src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt create mode 100644 src/test/java/com/android/tools/metalava/KeepFileTest.kt create mode 100644 src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt create mode 100644 src/test/java/com/android/tools/metalava/OptionsTest.kt create mode 100644 src/test/java/com/android/tools/metalava/PackageFilterTest.kt create mode 100644 src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt create mode 100644 src/test/java/com/android/tools/metalava/StubsTest.kt create mode 100644 src/test/java/com/android/tools/metalava/SystemServiceCheckTest.kt create mode 100644 src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt create mode 100644 src/test/java/com/android/tools/metalava/model/TextBackedAnnotationItemTest.kt create mode 100644 src/test/java/com/android/tools/metalava/model/TypeItemTest.kt create mode 100644 src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt create mode 100644 stub-annotations/build.gradle create mode 100644 stub-annotations/src/main/java/android/support/annotation/Migrate.java create mode 100644 stub-annotations/src/main/java/android/support/annotation/NewlyNonNull.java create mode 100644 stub-annotations/src/main/java/android/support/annotation/NewlyNullable.java create mode 100644 stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java create mode 100644 stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ddfee5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.iml +.gradle +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/out +.DS_Store diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..304a161 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <bytecodeTargetLevel> + <module name="metalava_main" target="1.8" /> + <module name="metalava_test" target="1.8" /> + </bytecodeTargetLevel> + </component> +</project> \ No newline at end of file diff --git a/.idea/copyright/aosp.xml b/.idea/copyright/aosp.xml new file mode 100644 index 0000000..090509b --- /dev/null +++ b/.idea/copyright/aosp.xml @@ -0,0 +1,6 @@ +<component name="CopyrightManager"> + <copyright> + <option name="notice" value="Copyright (C) &#36;today.year 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." /> + <option name="myName" value="aosp" /> + </copyright> +</component> \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..39c0f13 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ +<component name="CopyrightManager"> + <settings default="aosp" /> +</component> \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..267d99c --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="GradleSettings"> + <option name="linkedExternalProjectsSettings"> + <GradleProjectSettings> + <option name="distributionType" value="DEFAULT_WRAPPED" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="modules"> + <set> + <option value="$PROJECT_DIR$" /> + </set> + </option> + <option name="useAutoImport" value="true" /> + </GradleProjectSettings> + </option> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..84da703 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/classes" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a46b2c --- /dev/null +++ b/README.md @@ -0,0 +1,439 @@ +# Metalava + +(Also known as "doclava2", but deliberately not named doclava2 since crucially it +does not generate docs; it's intended only for **meta**data extraction and generation.) + +Metalava is a metadata generator intended for the Android source tree, used for +a number of purposes: + +* Allow extracting the API (into signature text files, into stub API files (which + in turn get compiled into android.jar, the Android SDK library) + and more importantly to hide code intended to be implementation only, driven + by javadoc comments like @hide, @$doconly, @removed, etc, as well as various + annotations. + +* Extracting source level annotations into external annotations file (such as + the typedef annotations, which cannot be stored in the SDK as .class level + annotations). + +* Diffing versions of the API and determining whether a newer version is compatible + with the older version. + +## Building and running + +To build: + + $ ./gradlew + +This builds a binary distribution in `../../out/host/common/install/metalava/bin/metalava`. + +To run metalava: + + $ ../../out/host/common/install/metalava/bin/metalava + _ _ + _ __ ___ ___| |_ __ _| | __ ___ ____ _ + | '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` | + | | | | | | __/ || (_| | | (_| |\ V / (_| | + |_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_| + + metalava extracts metadata from source code to generate artifacts such as the + signature files, the SDK stub files, external annotations etc. + + Usage: metalava <flags> + + Flags: + + --help This message. + --quiet Only include vital output + --verbose Include extra diagnostic output + + ... +(*output truncated*) + +Metalava has a new command line syntax, but it also understands the doclava1 +flags and translates them on the fly. Flags that are ignored are listed on +the command line. If metalava is dropped into an Android framework build for +example, you'll see something like this (unless running with --quiet) : + + metalava: Ignoring unimplemented doclava1 flag -encoding (UTF-8 assumed) + metalava: Ignoring unimplemented doclava1 flag -source (1.8 assumed) + metalava: Ignoring javadoc-related doclava1 flag -J-Xmx1600m + metalava: Ignoring javadoc-related doclava1 flag -J-XX:-OmitStackTraceInFastThrow + metalava: Ignoring javadoc-related doclava1 flag -XDignore.symbol.file + metalava: Ignoring javadoc-related doclava1 flag -doclet + metalava: Ignoring javadoc-related doclava1 flag -docletpath + metalava: Ignoring javadoc-related doclava1 flag -templatedir + metalava: Ignoring javadoc-related doclava1 flag -htmldir + ... + +## Features + +* Compatibility with doclava1: in compat mode, metalava spits out the same + signature files for the framework as doclava1. + +* Ability to read in an existing android.jar file instead of from source, which means + we can regenerate signature files etc for older versions according to new formats + (e.g. to fix past errors in doclava, such as annotation instance methods which were + accidentally not included.) + +* Ability to merge in data (annotations etc) from external sources, such as + IntelliJ external annotations data as well as signature files containing + annotations. This isn't just merged at export time, it's merged at codebase + load time such that it can be part of the API analysis. + +* Support for an updated signature file format: + + * Address errors in the doclava1 format which for example was missing annotation + class instance methods + + * Improve the signature format such that it for example labels enums "enum" + instead of "abstract class extends java.lang.Enum", annotations as "@interface" + instead of "abstract class extends java.lang.Annotation", sorts modifiers in + the canonical modifier order, using "extends" instead of "implements" for + the superclass of an interface, and many other similar tweaks outlined + in the `Compatibility` class. (Metalava also allows (and ignores) block + comments in the signature files.) + + * Add support for writing (and reading) annotations into the signature + files. This is vital now that some of these annotations become part of + the API contract (in particular nullness contracts, as well as parameter + names and default values.) + + * Support for a "compact" nullness format -- one based on Kotlin's syntax. Since + the goal is to have **all** API elements explicitly state their nullness + contract, the signature files would very quickly become bloated with + @NonNull and @Nullable annotations everywhere. So instead, the signature + format now uses a suffix of `?` for nullable, `!` for not yet annotated, and + nothing for non-null. + + Instead of + + method public java.lang.Double convert0(java.lang.Float); + method @Nullable public java.lang.Double convert1(@NonNull java.lang.Float); + + we have + + method public java.lang.Double! convert0(java.lang.Float!); + method public java.lang.Double? convert1(java.lang.Float); + + + * Other compactness improvements: Skip packages in some cases both for + export and reinsert during import. Specifically, drop "java.lang." + from package names such that you have + + method public void onUpdate(int, String); + + instead of + + method public void onUpdate(int, java.lang.String); + + Similarly, annotations (the ones considered part of the API; unknown + annotations are not included in signature files) use just the simple + name instead of the full package name, e.g. `@UiThread` instead of + `@android.annotation.UiThread`. + + * Misc documentation handling; for example, it attempts to fix sentences + that javadoc will mistreat, such as sentences that "end" with "e.g. ". + It also looks for various common typos and fixes those; here's a sample + error message running metalava on master: + Enhancing docs: + + frameworks/base/core/java/android/content/res/AssetManager.java:166: error: Replaced Kitkat with KitKat in documentation for Method android.content.res.AssetManager.getLocales() [Typo] + frameworks/base/core/java/android/print/PrinterCapabilitiesInfo.java:122: error: Replaced Kitkat with KitKat in documentation for Method android.print.PrinterCapabilitiesInfo.Builder.setColorModes(int, int) [Typo] + +* Built-in support for injecting new annotations for use by the Kotlin compiler, + not just nullness annotations found in the source code and annotations merged + in from external sources, but also inferring whether nullness annotations + have recently changed and if so marking them as @Migrate (which lets the + Kotlin compiler treat errors in the user code as warnings instead of errors.) + +* Support for generating documentation into the stubs files (so we can run javadoc or + [Dokka](https://github.com/Kotlin/dokka) on the stubs files instead of the source + code). This means that the documentation tool itself does not need to be able to + figure out which parts of the source code is included in the API and which one is + implementation; it is simply handed the filtered API stub sources that include + documentation. + +* Support for parsing Kotlin files. API files can now be implemented in Kotlin + as well and metalava will parse and extract API information from them just + as is done for Java files. + +* Like doclava1, metalava can diff two APIs and warn about API compatibility + problems such as removing API elements. Metalava adds new warnings around + nullness, such as attempting to change a nullness contract incompatibly + (e.g. you can change a parameter from non null to nullable for final classes, + but not versa). It also lets you diff directly on a source tree; it doesn't + require you to create two signature files to diff. + +* Consistent stubs: In doclava1, the code which iterated over the API and generated + the signature files and generated the stubs had diverged, so there was some + inconsistency. In metalava the stub files contain **exactly** the same signatures + as in the signature files. + +* 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: + + Nullness Annotation Coverage Statistics: + 1279 out of 46900 methods were annotated (2%) + 2 out of 21683 fields were annotated (0%) + 2770 out of 47492 parameters were annotated (5%) + + More importantly, you can also point it to some existing compiled applications + (.class or .jar files) and it will then measure the annotation coverage of + the APIs used by those applications. This lets us target the most important + APIs that are currently used by a corpus of apps and target our annotation + efforts in a targeted way. For example, running the analysis on the current + version of framework, and pointing it to the + [Plaid](https://github.com/nickbutcher/plaid) app's compiled output with + + ... --annotation-coverage-of ~/plaid/app/build/intermediates/classes/debug + + This produces the following output: + + 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 | + | android.view.View | 119 | + | android.view.ViewPropertyAnimator | 114 | + | android.content.Intent | 104 | + | android.graphics.Rect | 79 | + | android.content.Context | 61 | + | android.widget.TextView | 53 | + | android.transition.TransitionValues | 49 | + | android.animation.Animator | 34 | + | android.app.ActivityOptions | 34 | + | android.view.LayoutInflater | 31 | + | android.app.Activity | 28 | + | android.content.SharedPreferences | 26 | + | android.content.SharedPreferences.Editor | 26 | + | android.text.SpannableStringBuilder | 23 | + | android.view.ViewGroup.MarginLayoutParams | 21 | + | ... (99 more items | | + |--------------------------------------------------------------|------------------| + + Top referenced un-annotated members: + + |--------------------------------------------------------------|------------------| + | Member | Usage Count | + |--------------------------------------------------------------|-----------------:| + | Parcel.readString() | 62 | + | Parcel.writeString(String) | 62 | + | TextView.setText(CharSequence) | 34 | + | TransitionValues.values | 28 | + | View.getContext() | 28 | + | ViewPropertyAnimator.setDuration(long) | 26 | + | ViewPropertyAnimator.setInterpolator(android.animation.Ti... | 26 | + | LayoutInflater.inflate(int, android.view.ViewGroup, boole... | 23 | + | Rect.left | 22 | + | Rect.top | 22 | + | Intent.Intent(android.content.Context, Class<?>) | 21 | + | Rect.bottom | 21 | + | TransitionValues.view | 21 | + | VERSION.SDK_INT | 18 | + | 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 + and android.view.View for example where there are unannotated APIs that are + frequently used, at least by this app. + +* Built on top of a full, type-resolved AST. Doclava1 was integrated with javadoc, + which meant that most of the source tree was opaque. Therefore, as just one example, + the code which generated documentation for typedef constants had to require the + constants to all share a single prefix it could look for. However, in metalava, + annotation references are available at the AST level, so it can resolve references + and map them back to the original field references and include those directly. + +* Support for extracting annotations. Metalava can also generate the external annotation + files needed by Studio and lint in Gradle, which captures the typedefs (@IntDef and + @StringDef classes) in the source code. Prior to this this was generated manually + via the development/tools/extract code. This also merges in manually curated data; + some of this is in the manual/ folder in this project. + +* Support for extracting API levels (api-versions.xml). This was generated by separate + code (tools/base/misc/api-generator), invoked during the build. This functionality + is now rolled into metalava, which has one very important attribute: metalava + will use this information when recording API levels for API usage. (Prior to this, + this was based on signature file parsing in doclava, which sometimes generated + incorrect results. Metalava uses the android.jar files themselves to ensure that + it computes the exact available SDK data for each API level.) + +## Architecture & Implementation + +Metalava is implemented on top of IntelliJ parsing APIs (PSI and UAST). However, +these are hidden behind a "model": an abstraction layer which only exposes high +level concepts like packages, classes and inner classes, methods, fields, and +modifier lists (including annotations). + +This is done for multiple reasons: + +(1) It allows us to have multiple "back-ends": for example, metalava can read + in a model not just from parsing source code, but from reading older SDK + android.jar files (e.g. backed by bytecode) or reading previous signature + files. Reading in multiple versions of an API lets doclava perform "diffing", + such as warning if an API is changing in an incompatible way. It can also + generate signature files in the new format (including data that was missing + in older signature files, such as annotation methods) without having to + parse older source code which may no longer be easy to parse. + +(2) There's a lot of logic for deciding whether code found in the source tree + should be included in the API. With the model approach we can build up an + API and for example mark a subset of its methods as included. By having + a separate hierarchy we can easily perform this work once and pass around + our filtered model instead of passing around PsiClass and PsiMethod instances + and having to keep the filtered data separately and remembering to always + consult the filter, not the PSI elements directly. + +The basic API element class is "Item". (In doclava1 this was called a "DocInfo".) +There are several sub interfaces of Item: PackageItem, ClassItem, MemberItem, +MethodItem, FieldItem, ParameterItem, etc. And then there are several +implementation hierarchies: One is PSI based, where you point metalava to a +source tree or a .jar file, and it constructs Items built on top of PSI: +PsiPackageItem, PsiClassItem, PsiMethodItem, etc. Another is textual, based +on signature files: TextPackageItem, TextClassItem, and so on. + +The "Codebase" class captures a complete API snapshot (including classes +that are hidden, which is why it's called a "Codebase" rather than an "API"). + +There are methods to load codebases - from source folders, from a .jar file, +from a signature file. That's how API diffing is performed: you load two +codebases (from whatever source you want, typically a previous API signature +file and the current set of source folders), and then you "diff" the two. + +There are several key helpers that help with the implementation, detailed next. + +### Visiting Items + +First, metalava provides an ItemVisitor. This lets you visit the API easily. +For example, here's how you can visit every class: + + coebase.accept(object : ItemVisitor() { + override fun visitClass(cls: ClassItem) { + // code operating on the class here + } + }) + +Similarly you can visit all items (regardless of type) by overriding +`visitItem`, or to specifically visit methods, fields and so on +overriding `visitPackage`, `visitClass`, `visitMethod`, etc. + +There is also an `ApiVisitor`. This is a subclass of the `ItemVisitor`, +but which limits itself to visiting code elements that are part of the +API. + +This is how for example the SignatureWriter and the StubWriter are both +implemented: they simply extend `ApiVisitor`, which means they'll +only export the API items in the codebase, and then in each relevant +method they emit the signature or stub data: + + class SignatureWriter( + private val writer: PrintWriter, + private val generateDefaultConstructors: Boolean, + private val filter: (Item) -> Boolean) : ApiVisitor( + visitConstructorsAsMethods = false) { + + .... + + override fun visitConstructor(constructor: ConstructorItem) { + writer.print(" ctor ") + writeModifiers(constructor) + writer.print(constructor.containingClass().fullName()) + writeParameterList(constructor) + writeThrowsList(constructor) + writer.print(";\n") + } + + .... + +### Visiting Types + +There is a `TypeVisitor` similar to `ItemVisitor` which you can use +to visit all types in the codebase. + +When computing the API, all types that are included in the API should be +included (e.g. if `List<Foo>` is part of the API then `Foo` must be too). +This is easy to do with the `TypeVisitor`. + +### Diffing Codebases + +Another visitor which helps with implementation is the ComparisonVisitor: + + open class ComparisonVisitor { + open fun compare(old: Item, new: Item) {} + open fun added(item: Item) {} + open fun removed(item: Item) {} + + open fun compare(old: PackageItem, new: PackageItem) { } + open fun compare(old: ClassItem, new: ClassItem) { } + open fun compare(old: MethodItem, new: MethodItem) { } + open fun compare(old: FieldItem, new: FieldItem) { } + open fun compare(old: ParameterItem, new: ParameterItem) { } + + open fun added(item: PackageItem) { } + open fun added(item: ClassItem) { } + open fun added(item: MethodItem) { } + open fun added(item: FieldItem) { } + open fun added(item: ParameterItem) { } + + open fun removed(item: PackageItem) { } + open fun removed(item: ClassItem) { } + open fun removed(item: MethodItem) { } + open fun removed(item: FieldItem) { } + open fun removed(item: ParameterItem) { } + } + +This makes it easy to perform API comparison operations. + +For example, metalava has a feature to mark "newly annotated" nullness annotations +as migrated. To do this, it just extends `ComparisonVisitor`, overrides the +`compare(old: Item, new: Item)` method, and checks whether the old item +has no nullness annotations and the new one does, and if so, also marks +the new annotations as @Migrate. + +Similarly, the API Check can simply override + + open fun removed(item: Item) { + reporter.report(error, item, "Removing ${Item.describe(item)} is not allowed") + } + +to flag all API elements that have been removed as invalid (since you cannot +remove API.) + +### Documentation Generation + +As mentioned above, metalava generates documentation directly into the stubs +files, which can then be processed by Dokka and Javadoc to generate the +same docs as before. + +Doclava1 was integrated with javadoc directly, so the way it generated +metadata docs (such as documenting permissions, ranges and typedefs from +annotations) was to insert auxiliary tags (`@range`, `@permission`, etc) and +then this would get converted into English docs later via `macros_override.cs`. + +This it not how metalava does it; it generates the English documentation +directly. This was not just convenient for the implementation (since metalava +does not use javadoc data structures to pass maps like the arguments for +the typedef macro), but should also help Dokka -- and arguably the Kotlin +code which generates the documentation is easier to reason about and to +update when it's handling loop conditionals. (As a result I for example +improved some of the grammar, e.g. when it's listing a number of possible +constants the conjunction is usually "or", but if it's a flag, the sentence +begins with "a combination of " and then the conjunction at the end should +be "and"). + +## Current Status + +Some things are still missing before this tool can be integrated: + +- doclava1 had various error checking, and many of these have not been included yet + +- the code needs cleanup, and some performance optimizations (it's about 3x + slower than doclava1) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4aeeeb2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,96 @@ +buildscript { + ext.gradle_version = '3.1.0-alpha09' + ext.studio_version = '26.1.0-alpha09' + ext.kotlin_version = '1.2.21' + repositories { + google() + jcenter() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.android.tools.build:gradle:$gradle_version" + } +} + +apply plugin: 'application' +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'maven' + +group = 'com.android' +version = '0.9.0' + +mainClassName = "com.android.tools.metalava.Driver" +applicationDefaultJvmArgs = ["-ea", "-Xms2g", "-Xmx4g"] +sourceCompatibility = 1.8 + +compileKotlin { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + kotlinOptions { + jvmTarget = "1.8" + apiVersion = "1.2" + languageVersion = "1.2" + } +} + +repositories { + google() + jcenter() +} + +dependencies { + implementation "com.android.tools.external.org-jetbrains:uast:$studio_version" + implementation "com.android.tools.external.com-intellij:intellij-core:$studio_version" + implementation "com.android.tools.lint:lint-api:$studio_version" + implementation "com.android.tools.lint:lint-checks:$studio_version" + implementation "com.android.tools.lint:lint-gradle:$studio_version" + implementation "com.android.tools.lint:lint:$studio_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testImplementation 'junit:junit:4.11' + testImplementation "com.android.tools.lint:lint-tests:$studio_version" +} + +// shadow jar: Includes all dependencies +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2' + } +} +apply plugin: 'com.github.johnrengelman.shadow' +shadowJar { + baseName = "metalava-$version-full-SNAPSHOT" + classifier = null + version = null + zip64 = true +} + +defaultTasks 'installDist' + +/* + * With the build server you are given two env variables: + * 1. The OUT_DIR is a temporary directory you can use to put things during the build. + * 2. The DIST_DIR is where you want to save things from the build. + * + * The build server will copy the contents of DIST_DIR to somewhere and make it available. + */ +if (System.env.DIST_DIR != null && System.env.OUT_DIR != null) { + buildDir = file("${System.env.OUT_DIR}/host/common/metalava").getCanonicalFile() + ext.distDir = file(System.env.DIST_DIR).getCanonicalFile() + ext.distsDir = ext.distDir + + // The distDir is conveniently named after the build ID. + version = "${version}.${ext.distDir.name}" +} else { + buildDir = file('../../out/host/common') + ext.distDir = file('../../out/dist') + ext.distsDir = ext.distDir + + // Local builds are not public release candidates. + version = "${version}-SNAPSHOT" +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8998b2f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m +org.gradle.daemon=true +kotlin.incremental.usePreciseJavaTracking=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 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 Zc_R%5SO@)xU1S1!2k^J>_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..efc27cb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jan 13 09:12:34 PST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/manual/android/accounts/annotations.xml b/manual/android/accounts/annotations.xml new file mode 100644 index 0000000..f47e761 --- /dev/null +++ b/manual/android/accounts/annotations.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.accounts.Account> renameAccount(android.accounts.Account, java.lang.String, android.accounts.AccountManagerCallback<android.accounts.Account>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> addAccount(java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> confirmCredentials(android.accounts.Account, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> editProperties(java.lang.String, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.USE_CREDENTIALS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, boolean, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.USE_CREDENTIALS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> getAuthToken(android.accounts.Account, java.lang.String, boolean, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.USE_CREDENTIALS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> getAuthTokenByFeatures(java.lang.String, java.lang.String, java.lang.String[], android.app.Activity, android.os.Bundle, android.os.Bundle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> removeAccount(android.accounts.Account, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<android.os.Bundle> updateCredentials(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.AccountManagerFuture<java.lang.Boolean> removeAccount(android.accounts.Account, android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager boolean addAccountExplicitly(android.accounts.Account, java.lang.String, android.os.Bundle)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager boolean notifyAccountAuthenticated(android.accounts.Account)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager boolean removeAccountExplicitly(android.accounts.Account)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager java.lang.String blockingGetAuthToken(android.accounts.Account, java.lang.String, boolean)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.USE_CREDENTIALS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager java.lang.String getPassword(android.accounts.Account)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager java.lang.String getUserData(android.accounts.Account, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager java.lang.String peekAuthToken(android.accounts.Account, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.GET_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void clearPassword(android.accounts.Account)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MANAGE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void invalidateAuthToken(java.lang.String, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="anyOf" val="{"android.permission.MANAGE_ACCOUNTS", "android.permission.USE_CREDENTIALS"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void setAuthToken(android.accounts.Account, java.lang.String, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void setPassword(android.accounts.Account, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager void setUserData(android.accounts.Account, java.lang.String, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.AUTHENTICATE_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.Account[] getAccounts()"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.GET_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.accounts.AccountManager android.accounts.Account[] getAccountsByType(java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.GET_ACCOUNTS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> +</root> diff --git a/manual/android/content/annotations.xml b/manual/android/content/annotations.xml new file mode 100644 index 0000000..a69e90d --- /dev/null +++ b/manual/android/content/annotations.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="android.content.SharedPreferences java.lang.String getString(java.lang.String, java.lang.String)"> + <annotation name="org.jetbrains.annotations.Contract"> + <val name="value" val=""_,!null->!null"" /> + </annotation> + </item> + <item name="android.content.SharedPreferences java.util.Set<java.lang.String> getStringSet(java.lang.String, java.util.Set<java.lang.String>)"> + <annotation name="org.jetbrains.annotations.Contract"> + <val name="value" val=""_,!null->!null"" /> + </annotation> + </item> +</root> + diff --git a/manual/android/location/annotations.xml b/manual/android/location/annotations.xml new file mode 100644 index 0000000..9aa7540 --- /dev/null +++ b/manual/android/location/annotations.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <item name="android.location.LocationManager void removeProximityAlert(android.app.PendingIntent)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="anyOf" val="{"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.location.LocationManager void removeUpdates(android.location.LocationListener)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="anyOf" val="{"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> +</root> diff --git a/manual/android/provider/annotations.xml b/manual/android/provider/annotations.xml new file mode 100644 index 0000000..ab67042 --- /dev/null +++ b/manual/android/provider/annotations.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="android.provider.Browser BOOKMARKS_URI"> + <annotation name="android.support.annotation.RequiresPermission.Read"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + <annotation name="android.support.annotation.RequiresPermission.Write"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser SEARCHES_URI"> + <annotation name="android.support.annotation.RequiresPermission.Read"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + <annotation name="android.support.annotation.RequiresPermission.Write"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser android.database.Cursor getAllBookmarks(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser android.database.Cursor getAllVisitedUrls(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser boolean canClearHistory(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void addSearchUrl(android.content.ContentResolver, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="allOf" val="{"com.android.browser.permission.READ_HISTORY_BOOKMARKS", "com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void clearHistory(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void clearSearches(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void deleteFromHistory(android.content.ContentResolver, java.lang.String)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void deleteHistoryTimeFrame(android.content.ContentResolver, long, long)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void requestAllIcons(android.content.ContentResolver, java.lang.String, android.webkit.WebIconDatabase.IconListener)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void truncateHistory(android.content.ContentResolver)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="allOf" val="{"com.android.browser.permission.READ_HISTORY_BOOKMARKS", "com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> + <item name="android.provider.Browser void updateVisitedHistory(android.content.ContentResolver, java.lang.String, boolean)"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="allOf" val="{"com.android.browser.permission.READ_HISTORY_BOOKMARKS", "com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"}" /> + <val name="apis" val=""..22"" /> + </annotation> + </item> +</root> diff --git a/manual/android/support/design/widget/annotations.xml b/manual/android/support/design/widget/annotations.xml new file mode 100644 index 0000000..2aedd81 --- /dev/null +++ b/manual/android/support/design/widget/annotations.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="android.support.design.widget.Snackbar.Duration"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{android.support.design.widget.Snackbar.LENGTH_INDEFINITE, android.support.design.widget.Snackbar.LENGTH_SHORT, android.support.design.widget.Snackbar.LENGTH_LONG}" /> + </annotation> + </item> +</root> + diff --git a/manual/android/text/annotations.xml b/manual/android/text/annotations.xml new file mode 100644 index 0000000..0b15f6e --- /dev/null +++ b/manual/android/text/annotations.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="android.text.TextUtils boolean isEmpty(java.lang.CharSequence)"> + <annotation name="org.jetbrains.annotations.Contract"> + <val name="value" val=""null->true"" /> + </annotation> + </item> + <item name="android.text.TextUtils boolean stringOrSpannedString(java.lang.CharSequence)"> + <annotation name="org.jetbrains.annotations.Contract"> + <val name="value" val=""null>null;!null>!null"" /> + </annotation> + </item> +</root> + diff --git a/src/main/java/com/android/tools/lint/annotations/SdkUtils2.java b/src/main/java/com/android/tools/lint/annotations/SdkUtils2.java new file mode 100644 index 0000000..8e46634 --- /dev/null +++ b/src/main/java/com/android/tools/lint/annotations/SdkUtils2.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 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.lint.annotations; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +// Copy of SdkUtils but with a modification that isn't in SdkUtils yet + +public class SdkUtils2 { + /** + * Wraps the given text at the given line width, with an optional hanging + * indent. + * + * @param text the text to be wrapped + * @param lineWidth the number of characters to wrap the text to + * @param hangingIndent the hanging indent (to be used for the second and + * subsequent lines in each paragraph, or null if not known + * @return the string, wrapped + */ + @NonNull + public static String wrap( + @NonNull String text, + int lineWidth, + @Nullable String hangingIndent) { + return wrap(text, lineWidth, lineWidth, hangingIndent); + } + + /** + * Wraps the given text at the given line width, with an optional hanging + * indent. + * + * @param text the text to be wrapped + * @param firstLineWidth the line width to wrap the text to (on the first line) + * @param nextLineWidth the line width to wrap the text to (on subsequent lines). + * This does not include the hanging indent, if any. + * @param hangingIndent the hanging indent (to be used for the second and + * subsequent lines in each paragraph, or null if not known + * @return the string, wrapped + */ + @NonNull + public static String wrap( + @NonNull String text, + int firstLineWidth, + int nextLineWidth, + @Nullable String hangingIndent) { + if (hangingIndent == null) { + hangingIndent = ""; + } + int lineWidth = firstLineWidth; + int explanationLength = text.length(); + StringBuilder sb = new StringBuilder(explanationLength * 2); + int index = 0; + + while (index < explanationLength) { + int lineEnd = text.indexOf('\n', index); + int next; + + if (lineEnd != -1 && (lineEnd - index) < lineWidth) { + next = lineEnd + 1; + } else { + // Line is longer than available width; grab as much as we can + lineEnd = Math.min(index + lineWidth, explanationLength); + if (lineEnd - index < lineWidth) { + next = explanationLength; + } else { + // then back up to the last space + int lastSpace = text.lastIndexOf(' ', lineEnd); + if (lastSpace > index) { + lineEnd = lastSpace; + next = lastSpace + 1; + } else { + // No space anywhere on the line: it contains something wider than + // can fit (like a long URL) so just hard break it + next = lineEnd; + } + } + } + + if (sb.length() > 0) { + sb.append(hangingIndent); + } else { + lineWidth = nextLineWidth - hangingIndent.length(); + } + + sb.append(text.substring(index, lineEnd)); + sb.append('\n'); + index = next; + } + + return sb.toString(); + } +} diff --git a/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt b/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt new file mode 100644 index 0000000..38e79c5 --- /dev/null +++ b/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2017 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.lint.checks.infrastructure + +// ------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ +// +// Copy from lint; temporarily included in metalava sources since we need the latest +// version (from lint 3.1) which isn't available on maven.google.com yet. Delete this +// and replace with direct usage once it is. +// +// ------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ + +import java.util.regex.Pattern + +/** A pair of package name and class name inferred from Java or Kotlin source code */ +class ClassName(source: String) { + val packageName: String? + val className: String? + + init { + val withoutComments = stripComments(source) + packageName = getPackage(withoutComments) + className = getClassName(withoutComments) + } + + fun packageNameWithDefault() = packageName ?: "" +} + +/** + * Strips line and block comments from the given Java or Kotlin source file + */ +@Suppress("LocalVariableName") +fun stripComments(source: String, stripLineComments: Boolean = true): String { + val sb = StringBuilder(source.length) + var state = 0 + val INIT = 0 + val INIT_SLASH = 1 + val LINE_COMMENT = 2 + val BLOCK_COMMENT = 3 + val BLOCK_COMMENT_ASTERISK = 4 + val IN_STRING = 5 + val IN_STRING_ESCAPE = 6 + val IN_CHAR = 7 + val AFTER_CHAR = 8 + for (i in 0 until source.length) { + val c = source[i] + when (state) { + INIT -> { + when (c) { + '/' -> state = INIT_SLASH + '"' -> { + state = IN_STRING + sb.append(c) + } + '\'' -> { + state = IN_CHAR + sb.append(c) + } + else -> sb.append(c) + } + } + INIT_SLASH -> { + when { + c == '*' -> state = BLOCK_COMMENT + c == '/' && stripLineComments -> state = LINE_COMMENT + else -> { + state = INIT + sb.append('/') // because we skipped it in init + sb.append(c) + } + } + } + LINE_COMMENT -> { + when (c) { + '\n' -> state = INIT + } + } + BLOCK_COMMENT -> { + when (c) { + '*' -> state = BLOCK_COMMENT_ASTERISK + } + } + BLOCK_COMMENT_ASTERISK -> { + state = when (c) { + '/' -> INIT + '*' -> BLOCK_COMMENT_ASTERISK + else -> BLOCK_COMMENT + } + } + IN_STRING -> { + when (c) { + '\\' -> state = IN_STRING_ESCAPE + '"' -> state = INIT + } + sb.append(c) + } + IN_STRING_ESCAPE -> { + sb.append(c) + state = IN_STRING + } + IN_CHAR -> { + if (c != '\\') { + state = AFTER_CHAR + } + sb.append(c) + } + AFTER_CHAR -> { + sb.append(c) + if (c == '\\') { + state = INIT + } + } + } + } + + return sb.toString() +} + +private val PACKAGE_PATTERN = Pattern.compile("""package\s+([\S&&[^;]]*)""") + +private val CLASS_PATTERN = Pattern.compile("""(class|interface|enum|object)+?\s*([^\s:(]+)""", + Pattern.MULTILINE) + +fun getPackage(source: String): String? { + val matcher = PACKAGE_PATTERN.matcher(source) + return if (matcher.find()) { + matcher.group(1).trim { it <= ' ' } + } else { + null + } +} + +fun getClassName(source: String): String? { + val matcher = CLASS_PATTERN.matcher(source.replace('\n', ' ')) + var start = 0 + while (matcher.find(start)) { + val cls = matcher.group(2) + val groupStart = matcher.start(2) + + // Make sure this "class" reference isn't part of an annotation on the class + // referencing a class literal -- Foo.class, or in Kotlin, Foo::class.java) + if (groupStart == 0 || source[groupStart-1] != '.' && source[groupStart-1] != ':') { + val trimmed = cls.trim { it <= ' ' } + val typeParameter = trimmed.indexOf('<') + return if (typeParameter != -1) { + trimmed.substring(0, typeParameter) + } else { + trimmed + } + } + start = matcher.end(2) + } + + return null +} diff --git a/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt b/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt new file mode 100644 index 0000000..f82e67c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2017 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.SdkConstants +import com.android.tools.metalava.doclava1.ApiPredicate +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +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.PackageItem +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.google.common.io.ByteStreams +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import java.io.File +import java.io.IOException +import java.io.PrintWriter +import java.util.zip.ZipFile + +const val CLASS_COLUMN_WIDTH = 60 +const val COUNT_COLUMN_WIDTH = 16 +const val USAGE_REPORT_MAX_ROWS = 15 + +class AnnotationStatistics(val api: Codebase) { + val apiFilter = ApiPredicate(api) + + /** Measure the coverage statistics for the API */ + fun count() { + var allMethods = 0 + var annotatedMethods = 0 + var allFields = 0 + var annotatedFields = 0 + var allParameters = 0 + var annotatedParameters = 0 + + api.accept(object : ApiVisitor(api) { + override fun skip(item: Item): Boolean { + if (options.omitRuntimePackageStats && item is PackageItem) { + val name = item.qualifiedName() + if (name.startsWith("java.") || + name.startsWith("javax.") || + name.startsWith("kotlin.") || + name.startsWith("kotlinx.") + ) { + return true + } + } + return super.skip(item) + } + + override fun visitParameter(parameter: ParameterItem) { + allParameters++ + if (parameter.modifiers.annotations().any { it.isNonNull() || it.isNullable() }) { + annotatedParameters++ + } + } + + override fun visitField(field: FieldItem) { + allFields++ + if (field.modifiers.annotations().any { it.isNonNull() || it.isNullable() }) { + annotatedFields++ + } + } + + override fun visitMethod(method: MethodItem) { + allMethods++ + if (method.modifiers.annotations().any { it.isNonNull() || it.isNullable() }) { + annotatedMethods++ + } + } + }) + + options.stdout.println() + options.stdout.println( + """ + Nullness Annotation Coverage Statistics: + $annotatedMethods out of $allMethods methods were annotated (${percent(annotatedMethods, allMethods)}%) + $annotatedFields out of $allFields fields were annotated (${percent(annotatedFields, allFields)}%) + $annotatedParameters out of $allParameters parameters were annotated (${percent( + annotatedParameters, + allParameters + )}%) + """.trimIndent() + ) + } + + private fun percent(numerator: Int, denominator: Int): Int { + return if (denominator == 0) { + 0 + } else { + numerator * 100 / denominator + } + } + + fun measureCoverageOf(classPath: List<File>) { + val used = HashMap<MemberItem, Int>(1000) + + for (entry in classPath) { + recordUsages(used, entry, entry.path) + } + + // Keep only those items where there is at least one un-annotated element in the API + val filtered = used.keys.filter { + !it.hasNullnessInfo() + } + + val referenceCount = used.size + val missingCount = filtered.size + val annotatedCount = used.size - filtered.size + + // Sort by descending usage + val sorted = filtered.sortedWith(Comparator { o1, o2 -> + // Sort first by descending count, then increasing alphabetical + val delta = used[o2]!! - used[o1]!! + if (delta != 0) { + return@Comparator delta + } + o1.toString().compareTo(o2.toString()) + }) + + // High level summary + options.stdout.println() + options.stdout.println( + "$missingCount methods and fields were missing nullness annotations out of " + + "$referenceCount total API references." + ) + options.stdout.println("API nullness coverage is ${percent(annotatedCount, referenceCount)}%") + options.stdout.println() + + reportTopUnannotatedClasses(sorted, used) + printMemberTable(sorted, used) + } + + private fun reportTopUnannotatedClasses(sorted: List<MemberItem>, used: HashMap<MemberItem, Int>) { + // Aggregate class counts + val classCount = mutableMapOf<Item, Int>() + for (item in sorted) { + val containingClass = item.containingClass() + val itemCount = used[item]!! + val count = classCount[containingClass] + if (count == null) { + classCount[containingClass] = itemCount + } else { + classCount[containingClass] = count + itemCount + } + } + + // Print out top entries + val classes = classCount.keys.sortedWith(Comparator { o1, o2 -> + // Sort first by descending count, then increasing alphabetical + val delta = classCount[o2]!! - classCount[o1]!! + if (delta != 0) { + return@Comparator delta + } + o1.toString().compareTo(o2.toString()) + }) + + printClassTable(classes, classCount) + } + + /** Print table in clean Markdown table syntax */ + private fun printTable( + labelHeader: String, + countHeader: String, + items: List<Item>, + getLabel: (Item) -> String, + getCount: (Item) -> Int, + printer: PrintWriter = options.stdout + ) { + // Print table in clean Markdown table syntax + edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) + printer.printf( + "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}s |\n", + labelHeader, countHeader + ) + separator(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2, rightJustify = true) + + for (i in 0 until items.size) { + val item = items[i] + val label = getLabel(item) + val count = getCount(item) + printer.printf( + "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}d |\n", + truncate(label, CLASS_COLUMN_WIDTH), count + ) + + if (i == USAGE_REPORT_MAX_ROWS) { + printer.printf( + "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}s |\n", + "... (${items.size - USAGE_REPORT_MAX_ROWS} more items", "" + ) + break + } + } + edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) + } + + private fun printClassTable(classes: List<Item>, classCount: MutableMap<Item, Int>) { + printTable("Qualified Class Name", + "Usage Count", + classes, + { (it as ClassItem).qualifiedName() }, + { classCount[it]!! }) + } + + private fun printMemberTable( + sorted: List<MemberItem>, used: HashMap<MemberItem, Int>, + printer: PrintWriter = options.stdout + ) { + // Top APIs + printer.println("\nTop referenced un-annotated members:\n") + + printTable( + "Member", + "Usage Count", + sorted, + { + val member = it as MemberItem + "${member.containingClass().simpleName()}.${member.name()}${if (member is MethodItem) "(${member.parameters().joinToString { + it.type().toSimpleType() + }})" else ""}" + }, + { used[it]!! }, + printer + ) + } + + private fun dashes(printer: PrintWriter, max: Int) { + for (count in 0 until max) { + printer.print('-') + } + } + + private fun edge(printer: PrintWriter, column1: Int, column2: Int) { + printer.print("|") + dashes(printer, column1) + printer.print("|") + dashes(printer, column2) + printer.print("|") + printer.println() + } + + private fun separator(printer: PrintWriter, cell1: Int, cell2: Int, rightJustify: Boolean = false) { + printer.print('|') + dashes(printer, cell1) + printer.print('|') + if (rightJustify) { + dashes(printer, cell2 - 1) + // Markdown syntax to force column to be right justified instead of left justified + printer.print(":|") + } else { + dashes(printer, cell2) + printer.print('|') + } + printer.println() + } + + private fun truncate(string: String, maxLength: Int): String { + if (string.length < maxLength) { + return string + } + + return string.substring(0, maxLength - 3) + "..." + } + + private fun recordUsages(used: MutableMap<MemberItem, Int>, file: File, path: String) { + when { + file.name.endsWith(SdkConstants.DOT_JAR) -> try { + ZipFile(file).use({ jar -> + val enumeration = jar.entries() + while (enumeration.hasMoreElements()) { + val entry = enumeration.nextElement() + if (entry.name.endsWith(SdkConstants.DOT_CLASS)) { + try { + jar.getInputStream(entry).use({ `is` -> + val bytes = ByteStreams.toByteArray(`is`) + if (bytes != null) { + recordUsages(used, bytes, path + ":" + entry.name) + } + }) + } catch (e: Exception) { + options.stdout.println("Could not read jar file entry ${entry.name} from $file: $e") + } + } + } + }) + } catch (e: IOException) { + options.stdout.println("Could not read jar file contents from $file: $e") + } + file.isDirectory -> { + val listFiles = file.listFiles() + listFiles?.forEach { + recordUsages(used, it, it.path) + } + } + file.path.endsWith(SdkConstants.DOT_CLASS) -> { + val bytes = file.readBytes() + recordUsages(used, bytes, file.path) + } + else -> options.stdout.println("Ignoring entry $file") + } + } + + private fun recordUsages(used: MutableMap<MemberItem, Int>, bytes: ByteArray, path: String) { + val reader: ClassReader + val classNode: ClassNode + try { + reader = ClassReader(bytes) + classNode = ClassNode() + reader.accept(classNode, 0) + } catch (t: Throwable) { + options.stderr.println("Error processing $path: broken class file?") + return + } + + val skipJava = options.omitRuntimePackageStats + + for (methodObject in classNode.methods) { + val method = methodObject as MethodNode + val nodes = method.instructions + for (i in 0 until nodes.size()) { + val instruction = nodes.get(i) + val type = instruction.type + if (type == AbstractInsnNode.METHOD_INSN) { + val call = instruction as MethodInsnNode + if (skipJava && isSkippableOwner(call.owner)) { + continue + } + val item = findMethod(call) + item?.let { + val count = used[it] + if (count == null) { + used[it] = 1 + } else { + used[it] = count + 1 + } + } + } else if (type == AbstractInsnNode.FIELD_INSN) { + val field = instruction as FieldInsnNode + if (skipJava && isSkippableOwner(field.owner)) { + continue + } + val item = findField(field) + item?.let { + val count = used[it] + if (count == null) { + used[it] = 1 + } else { + used[it] = count + 1 + } + } + } + } + } + } + + private fun isSkippableOwner(owner: String) = + owner.startsWith("java/") || + owner.startsWith("javax/") || + owner.startsWith("kotlin") || + owner.startsWith("kotlinx/") + + private fun findField(node: FieldInsnNode): FieldItem? { + val cls = findClass(node.owner) ?: return null + val field = cls.findField(node.name) + return if (field != null && apiFilter.test(field)) { + field + } else { + null + } + } + + private fun findClass(owner: String): ClassItem? { + val className = owner.replace('/', '.').replace('$', '.') + val cls = api.findClass(className) + return if (cls != null && apiFilter.test(cls)) { + cls + } else { + null + } + } + + private fun findMethod(node: MethodInsnNode): MethodItem? { + val cls = findClass(node.owner) ?: return null + val types = Type.getArgumentTypes(node.desc) + val parameters = if (types.isNotEmpty()) { + val sb = StringBuilder() + for (type in types) { + if (!sb.isEmpty()) { + sb.append(", ") + } + sb.append(type.className.replace('/', '.').replace('$', '.')) + } + sb.toString() + } else { + "" + } + val methodName = if (node.name == "<init>") cls.simpleName() else node.name + val method = cls.findMethod(methodName, parameters) + return if (method != null && apiFilter.test(method)) { + method + } else { + null + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt new file mode 100644 index 0000000..d42bdac --- /dev/null +++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt @@ -0,0 +1,734 @@ +/* + * Copyright (C) 2017 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.SdkConstants.AMP_ENTITY +import com.android.SdkConstants.APOS_ENTITY +import com.android.SdkConstants.ATTR_NAME +import com.android.SdkConstants.DOT_CLASS +import com.android.SdkConstants.DOT_JAR +import com.android.SdkConstants.DOT_XML +import com.android.SdkConstants.DOT_ZIP +import com.android.SdkConstants.GT_ENTITY +import com.android.SdkConstants.INT_DEF_ANNOTATION +import com.android.SdkConstants.LT_ENTITY +import com.android.SdkConstants.QUOT_ENTITY +import com.android.SdkConstants.STRING_DEF_ANNOTATION +import com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE +import com.android.SdkConstants.TYPE_DEF_VALUE_ATTRIBUTE +import com.android.SdkConstants.VALUE_TRUE +import com.android.annotations.NonNull +import com.android.tools.lint.annotations.ApiDatabase +import com.android.tools.lint.annotations.Extractor.ANDROID_INT_DEF +import com.android.tools.lint.annotations.Extractor.ANDROID_NOTNULL +import com.android.tools.lint.annotations.Extractor.ANDROID_NULLABLE +import com.android.tools.lint.annotations.Extractor.ANDROID_STRING_DEF +import com.android.tools.lint.annotations.Extractor.ATTR_PURE +import com.android.tools.lint.annotations.Extractor.ATTR_VAL +import com.android.tools.lint.annotations.Extractor.IDEA_CONTRACT +import com.android.tools.lint.annotations.Extractor.IDEA_MAGIC +import com.android.tools.lint.annotations.Extractor.IDEA_NOTNULL +import com.android.tools.lint.annotations.Extractor.IDEA_NULLABLE +import com.android.tools.lint.annotations.Extractor.SUPPORT_NOTNULL +import com.android.tools.lint.annotations.Extractor.SUPPORT_NULLABLE +import com.android.tools.lint.detector.api.LintUtils.getChildren +import com.android.tools.metalava.model.AnnotationAttribute +import com.android.tools.metalava.model.AnnotationAttributeValue +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultAnnotationValue +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.psi.PsiAnnotationItem +import com.android.utils.XmlUtils +import com.google.common.base.Charsets +import com.google.common.base.Splitter +import com.google.common.collect.ImmutableSet +import com.google.common.io.ByteStreams +import com.google.common.io.Closeables +import com.google.common.io.Files +import com.google.common.xml.XmlEscapers +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.xml.sax.SAXParseException +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.lang.reflect.Field +import java.util.ArrayList +import java.util.Collections +import java.util.HashMap +import java.util.jar.JarInputStream +import java.util.regex.Pattern +import java.util.zip.ZipEntry +import kotlin.Comparator + +/** Merges annotations into classes already registered in the given [Codebase] */ +class AnnotationsMerger( + private val codebase: Codebase, + private val apiFilter: ApiDatabase?, + private val listIgnored: Boolean = true +) { + fun merge(mergeAnnotations: List<File>) { + mergeAnnotations.forEach { mergeExisting(it) } + } + + private fun mergeExisting(@NonNull file: File) { + if (file.isDirectory) { + val files = file.listFiles() + if (files != null) { + for (child in files) { + mergeExisting(child) + } + } + } else if (file.isFile) { + if (file.path.endsWith(DOT_JAR) || file.path.endsWith(DOT_ZIP)) { + mergeFromJar(file) + } else if (file.path.endsWith(DOT_XML)) { + try { + val xml = Files.asCharSource(file, Charsets.UTF_8).read() + mergeAnnotationsXml(file.path, xml) + } catch (e: IOException) { + error("Aborting: I/O problem during transform: " + e.toString()) + } + + } + } + } + + private fun mergeFromJar(@NonNull jar: File) { + // Reads in an existing annotations jar and merges in entries found there + // with the annotations analyzed from source. + var zis: JarInputStream? = null + try { + val fis = FileInputStream(jar) + zis = JarInputStream(fis) + var entry: ZipEntry? = zis.nextEntry + while (entry != null) { + if (entry.name.endsWith(".xml")) { + val bytes = ByteStreams.toByteArray(zis) + val xml = String(bytes, Charsets.UTF_8) + mergeAnnotationsXml(jar.path + ": " + entry, xml) + } + entry = zis.nextEntry + } + } catch (e: IOException) { + error("Aborting: I/O problem during transform: " + e.toString()) + } finally { + try { + Closeables.close(zis, true /* swallowIOException */) + } catch (e: IOException) { + // cannot happen + } + } + } + + private fun mergeAnnotationsXml(@NonNull path: String, @NonNull xml: String) { + try { + val document = XmlUtils.parseDocument(xml, false) + mergeDocument(document) + } catch (e: Exception) { + var message = "Failed to merge " + path + ": " + e.toString() + if (e is SAXParseException) { + message = "Line " + e.lineNumber + ":" + e.columnNumber + ": " + message + } + error(message) + if (e !is IOException) { + e.printStackTrace() + } + } + } + + internal fun error(message: String) { + // TODO: Integrate with metalava error facility + options.stderr.println("Error: " + message) + } + + internal fun warning(message: String) { + options.stdout.println("Warning: " + message) + } + + @Suppress("PrivatePropertyName") + private val XML_SIGNATURE: Pattern = Pattern.compile( + // Class (FieldName | Type? Name(ArgList) Argnum?) + //"(\\S+) (\\S+|(.*)\\s+(\\S+)\\((.*)\\)( \\d+)?)"); + "(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)" + ) + + private fun mergeDocument(@NonNull document: Document) { + + val root = document.documentElement + val rootTag = root.tagName + assert(rootTag == "root") { rootTag } + + for (item in getChildren(root)) { + var signature: String? = item.getAttribute(ATTR_NAME) + if (signature == null || signature == "null") { + continue // malformed item + } + + signature = unescapeXml(signature) + if (signature == "java.util.Calendar int get(int)") { + // https://youtrack.jetbrains.com/issue/IDEA-137385 + continue + } else if (signature == "java.util.Calendar void set(int, int, int) 1" + || signature == "java.util.Calendar void set(int, int, int, int, int) 1" + || signature == "java.util.Calendar void set(int, int, int, int, int, int) 1" + ) { + // http://b.android.com/76090 + continue + } + + val matcher = XML_SIGNATURE.matcher(signature) + if (matcher.matches()) { + val containingClass = matcher.group(1) + if (containingClass == null) { + warning("Could not find class for " + signature) + continue + } + + if (apiFilter != null && + !hasHistoricData(item) && + !apiFilter.hasClass(containingClass) + ) { + if (listIgnored) { + warning("Skipping imported element because it is not part of the API file: $containingClass") + } + continue + } + + val classItem = codebase.findClass(containingClass) + if (classItem == null) { + warning("Could not find class $containingClass; omitting annotations merge") + continue + } + + val methodName = matcher.group(5) + if (methodName != null) { + val parameters = matcher.group(6) + val parameterIndex = + if (matcher.group(7) != null) { + Integer.parseInt(matcher.group(7).trim()) + } else { + -1 + } + mergeMethodOrParameter(item, containingClass, classItem, methodName, parameterIndex, parameters) + } else { + val fieldName = matcher.group(2) + mergeField(item, containingClass, classItem, fieldName) + } + } else if (signature.indexOf(' ') == -1 && signature.indexOf('.') != -1) { + // Must be just a class + val containingClass = signature + if (apiFilter != null && + !hasHistoricData(item) && + !apiFilter.hasClass(containingClass) + ) { + if (listIgnored) { + warning("Skipping imported element because it is not part of the API file: $containingClass") + } + continue + } + + val classItem = codebase.findClass(containingClass) + if (classItem == null) { + warning("Could not find class $containingClass; omitting annotations merge") + continue + } + + mergeAnnotations(item, classItem) + } else { + warning("No merge match for signature " + signature) + } + } + } + + // The parameter declaration used in XML files should not have duplicated spaces, + // and there should be no space after commas (we can't however strip out all spaces, + // since for example the spaces around the "extends" keyword needs to be there in + // types like Map<String,? extends Number> + private fun fixParameterString(parameters: String): String { + return parameters.replace(" ", " ").replace(", ", ",").replace("?super", "? super ") + .replace("?extends", "? extends ") + } + + private fun mergeMethodOrParameter( + item: Element, containingClass: String, classItem: ClassItem, + methodName: String, parameterIndex: Int, + parameters: String + ) { + @Suppress("NAME_SHADOWING") + val parameters = fixParameterString(parameters) + + if (apiFilter != null && + !hasHistoricData(item) && + !apiFilter.hasMethod(containingClass, methodName, parameters) + ) { + if (listIgnored) { + warning( + "Skipping imported element because it is not part of the API file: " + + containingClass + "#" + methodName + "(" + parameters + ")" + ) + } + return + } + + val methodItem: MethodItem? = classItem.findMethod(methodName, parameters) + if (methodItem == null) { + warning("Could not find class $methodName($parameters) in $containingClass; omitting annotations merge") + return + } + + if (parameterIndex != -1) { + val parameterItem = methodItem.parameters()[parameterIndex] + + if ("java.util.Calendar" == containingClass && "set" == methodName + && parameterIndex > 0 + ) { + // Skip the metadata for Calendar.set(int, int, int+); see + // https://code.google.com/p/android/issues/detail?id=73982 + return + } + + mergeAnnotations(item, parameterItem) + } else { + // Annotation on the method itself + mergeAnnotations(item, methodItem) + } + } + + private fun mergeField(item: Element, containingClass: String, classItem: ClassItem, fieldName: String) { + if (apiFilter != null && + !hasHistoricData(item) && + !apiFilter.hasField(containingClass, fieldName) + ) { + if (listIgnored) { + warning( + "Skipping imported element because it is not part of the API file: " + + containingClass + "#" + fieldName + ) + } + } else { + val fieldItem = classItem.findField(fieldName) + if (fieldItem == null) { + warning("Could not find field $fieldName in $containingClass; omitting annotations merge") + return + } + + mergeAnnotations(item, fieldItem) + } + } + + private fun getAnnotationName(element: Element): String { + val tagName = element.tagName + assert(tagName == "annotation") { tagName } + + val qualifiedName = element.getAttribute(ATTR_NAME) + assert(qualifiedName != null && !qualifiedName.isEmpty()) + return qualifiedName + } + + private fun mergeAnnotations(xmlElement: Element, item: Item): Int { + var count = 0 + + loop@ for (annotationElement in getChildren(xmlElement)) { + val qualifiedName = getAnnotationName(annotationElement) + if (!AnnotationItem.isSignificantAnnotation(qualifiedName)) { + continue + } + var haveNullable = false + var haveNotNull = false + for (existing in item.modifiers.annotations()) { + val name = existing.qualifiedName() ?: continue + if (isNonNull(name)) { + haveNotNull = true + } + if (isNullable(name)) { + haveNullable = true + } + if (name == qualifiedName) { + continue@loop + } + } + + // Make sure we don't have a conflict between nullable and not nullable + if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) { + warning("Found both @Nullable and @NonNull after import for " + item) + continue + } + + val annotationItem = createAnnotation(annotationElement) ?: continue + item.mutableModifiers().addAnnotation(annotationItem) + count++ + } + + return count + } + + /** Reads in annotation data from an XML item (using IntelliJ IDE's external annotations XML format) and + * creates a corresponding [AnnotationItem], performing some "translations" in the process (e.g. mapping + * from IntelliJ annotations like `org.jetbrains.annotations.Nullable` to `android.support.annotation.Nullable`, + * as well as dropping constants from typedefs that aren't included according to the [apiFilter]. */ + private fun createAnnotation(annotationElement: Element): AnnotationItem? { + val tagName = annotationElement.tagName + assert(tagName == "annotation") { tagName } + val name = annotationElement.getAttribute(ATTR_NAME) + assert(name != null && !name.isEmpty()) + when { + name == IDEA_MAGIC -> { + val children = getChildren(annotationElement) + assert(children.size == 1) { children.size } + val valueElement = children[0] + val valName = valueElement.getAttribute(ATTR_NAME) + var value = valueElement.getAttribute(ATTR_VAL) + val flagsFromClass = valName == "flagsFromClass" + val flag = valName == "flags" || flagsFromClass + if (valName == "valuesFromClass" || flagsFromClass) { + // Not supported + var found = false + if (value.endsWith(DOT_CLASS)) { + val clsName = value.substring(0, value.length - DOT_CLASS.length) + val sb = StringBuilder() + sb.append('{') + + var reflectionFields: Array<Field>? = null + try { + val cls = Class.forName(clsName) + reflectionFields = cls.declaredFields + } catch (ignore: Exception) { + // Class not available: not a problem. We'll rely on API filter. + // It's mainly used for sorting anyway. + } + + if (apiFilter != null) { + // Search in API database + var fields: Set<String>? = apiFilter.getDeclaredIntFields(clsName) + if ("java.util.zip.ZipEntry" == clsName) { + // The metadata says valuesFromClass ZipEntry, and unfortunately + // that class implements ZipConstants and therefore imports a large + // number of irrelevant constants that aren't valid here. Instead, + // only allow these two: + fields = ImmutableSet.of("STORED", "DEFLATED") + } + + if (fields != null) { + val sorted = ArrayList(fields) + Collections.sort(sorted) + if (reflectionFields != null) { + val rank = HashMap<String, Int>() + run { + var i = 0 + val n = sorted.size + while (i < n) { + rank.put(sorted[i], reflectionFields.size + i) + i++ + + } + } + var i = 0 + val n = reflectionFields.size + while (i < n) { + rank.put(reflectionFields[i].name, i) + i++ + } + sorted.sortWith(Comparator { o1, o2 -> + val rank1 = rank[o1] + val rank2 = rank[o2] + val delta = rank1!! - rank2!! + if (delta != 0) { + return@Comparator delta + + } + o1.compareTo(o2) + }) + } + var first = true + for (field in sorted) { + if (first) { + first = false + } else { + sb.append(',').append(' ') + } + sb.append(clsName).append('.').append(field) + } + found = true + } + } + // Attempt to sort in reflection order + if (!found && reflectionFields != null && (apiFilter == null || apiFilter.hasClass(clsName))) { + // Attempt with reflection + var first = true + for (field in reflectionFields) { + if (field.type == Integer.TYPE || field.type == Int::class.javaPrimitiveType) { + if (first) { + first = false + } else { + sb.append(',').append(' ') + } + sb.append(clsName).append('.').append(field.name) + } + } + } + sb.append('}') + value = sb.toString() + if (sb.length > 2) { // 2: { } + found = true + } + } + + if (!found) { + return null + } + } + + if (apiFilter != null) { + value = removeFiltered(value) + while (value.contains(", ,")) { + value = value.replace(", ,", ",") + } + if (value.startsWith(", ")) { + value = value.substring(2) + } + } + + val attributes = mutableListOf<XmlBackedAnnotationAttribute>() + attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) + if (flag) { + attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_FLAG_ATTRIBUTE, VALUE_TRUE)) + } + return PsiAnnotationItem.create( + codebase, XmlBackedAnnotationItem( + codebase, + if (valName == "stringValues") STRING_DEF_ANNOTATION else INT_DEF_ANNOTATION, attributes + ) + ) + } + + name == STRING_DEF_ANNOTATION || + name == ANDROID_STRING_DEF || + name == INT_DEF_ANNOTATION || + name == ANDROID_INT_DEF -> { + val children = getChildren(annotationElement) + var valueElement = children[0] + val valName = valueElement.getAttribute(ATTR_NAME) + assert(TYPE_DEF_VALUE_ATTRIBUTE == valName) + val value = valueElement.getAttribute(ATTR_VAL) + var flag = false + if (children.size == 2) { + valueElement = children[1] + assert(TYPE_DEF_FLAG_ATTRIBUTE == valueElement.getAttribute(ATTR_NAME)) + flag = VALUE_TRUE == valueElement.getAttribute(ATTR_VAL) + } + val intDef = INT_DEF_ANNOTATION == name || ANDROID_INT_DEF == name + + val attributes = mutableListOf<XmlBackedAnnotationAttribute>() + attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) + if (flag) { + attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_FLAG_ATTRIBUTE, VALUE_TRUE)) + } + return PsiAnnotationItem.create( + codebase, XmlBackedAnnotationItem( + codebase, + if (intDef) INT_DEF_ANNOTATION else STRING_DEF_ANNOTATION, attributes + ) + ) + } + + name == IDEA_CONTRACT -> { + val children = getChildren(annotationElement) + val valueElement = children[0] + val value = valueElement.getAttribute(ATTR_VAL) + val pure = valueElement.getAttribute(ATTR_PURE) + return if (pure != null && !pure.isEmpty()) { + PsiAnnotationItem.create( + codebase, XmlBackedAnnotationItem( + codebase, name, + listOf( + XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value), + XmlBackedAnnotationAttribute(ATTR_PURE, pure) + ) + ) + ) + } else { + PsiAnnotationItem.create( + codebase, XmlBackedAnnotationItem( + codebase, name, + listOf(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) + ) + ) + } + } + + isNonNull(name) -> return codebase.createAnnotation("@$SUPPORT_NOTNULL") + + isNullable(name) -> return codebase.createAnnotation("@$SUPPORT_NULLABLE") + + else -> { + val children = getChildren(annotationElement) + if (children.isEmpty()) { + return codebase.createAnnotation("@$name") + } + val attributes = mutableListOf<XmlBackedAnnotationAttribute>() + for (valueElement in children) { + attributes.add( + XmlBackedAnnotationAttribute( + valueElement.getAttribute(ATTR_NAME) ?: continue, + valueElement.getAttribute(ATTR_VAL) ?: continue + ) + ) + } + return PsiAnnotationItem.create(codebase, XmlBackedAnnotationItem(codebase, name, attributes)) + } + } + } + + private fun removeFiltered(originalValue: String): String { + var value = originalValue + assert(apiFilter != null) + if (value.startsWith("{")) { + value = value.substring(1) + } + if (value.endsWith("}")) { + value = value.substring(0, value.length - 1) + } + value = value.trim { it <= ' ' } + val sb = StringBuilder(value.length) + sb.append('{') + for (escaped in Splitter.on(',').omitEmptyStrings().trimResults().split(value)) { + val fqn = unescapeXml(escaped) + if (fqn.startsWith("\"")) { + continue + } + val index = fqn.lastIndexOf('.') + val cls = fqn.substring(0, index) + val field = fqn.substring(index + 1) + if (apiFilter?.hasField(cls, field) != false) { + if (sb.length > 1) { // 0: '{' + sb.append(", ") + } + sb.append(fqn) + } else if (listIgnored) { + warning("Skipping constant from typedef because it is not part of the SDK: " + fqn) + } + } + sb.append('}') + return escapeXml(sb.toString()) + } + + private fun isNonNull(name: String): Boolean { + return name == IDEA_NOTNULL + || name == ANDROID_NOTNULL + || name == SUPPORT_NOTNULL + } + + private fun isNullable(name: String): Boolean { + return name == IDEA_NULLABLE + || name == ANDROID_NULLABLE + || name == SUPPORT_NULLABLE + } + + /** + * Returns true if this XML entry contains historic metadata, e.g. has + * an api attribute which designates that this API may no longer be in the SDK, + * but the annotations should be preserved for older API levels + */ + private fun hasHistoricData(@NonNull item: Element): Boolean { + var curr: Node? = item.firstChild + while (curr != null) { + // Example: + // <item name="android.provider.Browser BOOKMARKS_URI"> + // <annotation name="android.support.annotation.RequiresPermission.Read"> + // <val name="value" val=""com.android.browser.permission.READ_HISTORY_BOOKMARKS"" /> + // <val name="apis" val=""..22"" /> + // </annotation> + // .. + if (curr.nodeType == Node.ELEMENT_NODE && "annotation" == curr.nodeName) { + var inner: Node? = curr.firstChild + while (inner != null) { + if (inner.nodeType == Node.ELEMENT_NODE && + "val" == inner.nodeName && + "apis" == (inner as Element).getAttribute("name") + ) { + return true + } + inner = inner.nextSibling + } + } + curr = curr.nextSibling + } + + return false + } + + @NonNull + private fun escapeXml(@NonNull unescaped: String): String { + return XmlEscapers.xmlAttributeEscaper().escape(unescaped) + } + + @NonNull + private fun unescapeXml(@NonNull escaped: String): String { + var workingString = escaped.replace(QUOT_ENTITY, "\"") + workingString = workingString.replace(LT_ENTITY, "<") + workingString = workingString.replace(GT_ENTITY, ">") + workingString = workingString.replace(APOS_ENTITY, "'") + workingString = workingString.replace(AMP_ENTITY, "&") + + return workingString + } +} + +// TODO: Replace with usage of DefaultAnnotationValue? +data class XmlBackedAnnotationAttribute( + override val name: String, + private val valueLiteral: String +) : AnnotationAttribute { + override val value: AnnotationAttributeValue = DefaultAnnotationValue.create(valueLiteral) + + override fun toString(): String { + return "$name=$valueLiteral" + } +} + +// TODO: Replace with usage of DefaultAnnotationAttribute? +class XmlBackedAnnotationItem( + override var codebase: Codebase, + private val qualifiedName: String, + private val attributes: List<XmlBackedAnnotationAttribute> = emptyList() +) : AnnotationItem { + override fun qualifiedName(): String? = AnnotationItem.mapName(codebase, qualifiedName) + + override fun attributes() = attributes + + override fun toSource(): String { + val qualifiedName = qualifiedName() ?: return "" + + if (attributes.isEmpty()) { + return "@" + qualifiedName + } + + val sb = StringBuilder(30) + sb.append("@") + sb.append(qualifiedName) + sb.append("(") + attributes.joinTo(sb) + sb.append(")") + + return sb.toString() + } +} diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt new file mode 100644 index 0000000..bf36dda --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt @@ -0,0 +1,1085 @@ +/* + * Copyright (C) 2017 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.doclava1.Errors +import com.android.tools.metalava.model.AnnotationAttributeValue +import com.android.tools.metalava.model.ClassItem +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.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.PackageList +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.VisibleItemVisitor +import java.util.ArrayList +import java.util.HashMap +import java.util.HashSet +import java.util.function.Predicate + +/** + * The [ApiAnalyzer] is responsible for walking over the various + * classes and members and compute visibility etc of the APIs + */ +class ApiAnalyzer( + /** The code to analyze */ + private val codebase: Codebase +) { + /** All packages in the API */ + private val packages: PackageList = codebase.getPackages() + + fun computeApi() { + if (codebase.trustedApi()) { + // The codebase is already an API; no consistency checks to be performed + return + } + + // Apply options for packages that should be hidden + hidePackages() + skipEmitPackages() + + // Propagate visibility down into individual elements -- if a class is hidden, + // then the methods and fields are hidden etc + propagateHiddenRemovedAndDocOnly(false) + } + + fun addConstructors(filter: Predicate<Item>) { + // Let's say I have + // class GrandParent { public GrandParent(int) {} } + // class Parent { Parent(int) {} } + // class Child { public Child(int) {} } + // + // Here Parent's constructor is not public. For normal stub generation I'd end up with this: + // class GrandParent { public GrandParent(int) {} } + // class Parent { } + // class Child { public Child(int) {} } + // + // This doesn't compile - Parent can't have a default constructor since there isn't + // one for it to invoke on GrandParent. + // + // I can generate a fake constructor instead, such as + // Parent() { super(0); } + // + // But it's hard to do this lazily; what if I'm generating the Child class first? + // Therefore, we'll instead walk over the hierarchy and insert these constructors + // into the Item hierarchy such that code generation can find them. + // + // (We also need to handle the throws list, so we can't just unconditionally + // insert package private constructors + // + // To do this right I really need to process super constructors before the classes + // depending on them. + + // Mark all classes that are the super class of some other class: + val allClasses = packages.allClasses().filter { filter.test(it) } + + codebase.clearTags() + allClasses.forEach { cls -> + cls.superClass()?.tag = true + } + + val leafClasses = allClasses.filter { !it.tag }.toList() + + // Now walk through all the leaf classes, and walk up the super hierarchy + // and recursively add constructors; we'll do it recursively to make sure that + // the superclass has had its constructors initialized first (such that we can + // match the parameter lists and throws signatures), and we use the tag fields + // to avoid looking at all the internal classes more than once. + codebase.clearTags() + leafClasses + // Filter classes by filter here to not waste time in hidden packages + .filter { filter.test(it) } + .forEach { addConstructors(it, filter, true) } + } + + /** + * Handle computing constructor hierarchy. We'll be setting several attributes: + * [ClassItem.defaultConstructor] : The default constructor to invoke in this + * class from subclasses. **NOTE**: This constructor may not be part of + * the [ClassItem.constructors] list, e.g. for package private default constructors + * we've inserted (because there were no public constructors or constructors not + * using hidden parameter types.) + * + * If we can find a public constructor we'll put that here instead. + * + * [ConstructorItem.superConstructor] The default constructor to invoke. If set, + * use this rather than the [ClassItem.defaultConstructor]. + * + * [ClassItem.hasPrivateConstructor] Set if this class has one or more private + * constructors. + * + * [Item.tag] : mark for avoiding repeated iteration of internal item nodes + * + * + */ + private fun addConstructors(cls: ClassItem, filter: Predicate<Item>, isLeaf: Boolean) { + // What happens if we have + // package foo: + // public class A { public A(int) } + // package bar + // public class B extends A { public B(int) } + // If I just try inserting package private constructors here things will NOT work: + // package foo: + // public class A { public A(int); A() {} } + // package bar + // public class B extends A { public B(int); B() } + // because A <() is not accessible from B() -- it's outside the same package. + // + // So, I'll need to model the real constructors for all the scenarios where that + // works. + // + // The remaining challenge is that there will be some gaps: when I don't have + // a default constructor, subclass constructors will have to have an explicit + // super(args) call to pick the parent constructor to use. And which one? + // It generally doesn't matter; just pick one, but unfortunately, the super + // constructor can throw exceptions, and in that case the subclass constructor + // must also throw all those constructors (you can't surround a super call + // with try/catch.) Luckily, the source code already needs to do this to + // compile, so we can just use the same constructor as the super call. + // But there are two cases we have to deal with: + // (1) the constructor doesn't call a super constructor; it calls another + // constructor on this class. + // (2) the super constructor it *does* call isn't available. + // + // For (1), this means that our stub code generator should be prepared to + // handle both super- and this- dispatches; we'll handle this by pointing + // it to the constructor to use, and it checks to see if the containing class + // for the constructor is the same to decide whether to emit "this" or "super". + + if (cls.tag || !cls.isClass()) { // Don't add constructors to interfaces, enums, annotations, etc + return + } + + // First handle its super class hierarchy to make sure that we've + // already constructed super classes + val superClass = cls.filteredSuperclass(filter) + superClass?.let { it -> addConstructors(it, filter, false) } + cls.tag = true + + if (superClass != null) { + val superDefaultConstructor = superClass.defaultConstructor + if (superDefaultConstructor != null) { + val constructors = cls.constructors() + for (constructor in constructors) { + val superConstructor = constructor.superConstructor + if (superConstructor == null || + (superConstructor.containingClass() != superClass && + superConstructor.containingClass() != cls) + ) { + constructor.superConstructor = superDefaultConstructor + } + } + } + } + + // Find default constructor, if one doesn't exist + if (!isLeaf || cls.hasPrivateConstructor || cls.constructors().isNotEmpty()) { + val constructors = cls.constructors() + for (constructor in constructors) { + if (constructor.parameters().isEmpty() && constructor.isPublic) { + cls.defaultConstructor = constructor + return + } + } + + if (!constructors.isEmpty()) { + // Try to pick the constructor, sorting first by "matches filter", then by + // uses available types, then by fewest throwables, then fewest parameters, + // then based on order in listfilter.test(cls) + val first = constructors.first() + val best = + if (constructors.size > 1) { + constructors.foldRight(first) { current, next -> pickBest(current, next, filter) } + } else { + first + } + + if (cls.filteredConstructors(filter).contains(best)) { + cls.defaultConstructor = best + return + } + + if (!referencesExcludedType(best, filter)) { + cls.defaultConstructor = best + best.mutableModifiers().setPackagePrivate(true) + best.hidden = false + best.docOnly = false + return + } + } + + // No constructors, yet somebody extends this (or private constructor): we have to invent one, such that + // subclasses can dispatch to it in the stub files etc + cls.defaultConstructor = cls.createDefaultConstructor().also { + it.mutableModifiers().setPackagePrivate(true) + it.hidden = false + it.superConstructor = superClass?.defaultConstructor; + } + } + } + + private fun referencesExcludedType(constructor: ConstructorItem, filter: Predicate<Item>): Boolean { + // Checks parameter types and throws types + for (parameter in constructor.parameters()) { + val type = parameter.type() + if (type.referencesExcludedType(filter)) { + return true + } + } + for (cls in constructor.throwsTypes()) { + if (!filter.test(cls)) { + return true + } + } + + return false + } + + // TODO: Annotation test: @ParameterName, if present, must be supplied on *all* the arguments! + // Warn about @DefaultValue("null"); they probably meant @DefaultNull + // Supplying default parameter in override is not allowed! + + private fun pickBest( + current: ConstructorItem, + next: ConstructorItem, + filter: Predicate<Item> + ): ConstructorItem { + val currentMatchesFilter = filter.test(current) + val nextMatchesFilter = filter.test(next) + if (currentMatchesFilter != nextMatchesFilter) { + return if (currentMatchesFilter) { + current + } else { + next + } + } + + val currentUsesAvailableTypes = !referencesExcludedType(current, filter) + val nextUsesAvailableTypes = !referencesExcludedType(current, filter) + if (currentUsesAvailableTypes != nextUsesAvailableTypes) { + return if (currentUsesAvailableTypes) { + current + } else { + next + } + } + + val currentThrowsCount = current.throwsTypes().size + val nextThrowsCount = next.throwsTypes().size + + return if (currentThrowsCount < nextThrowsCount) { + current + } else if (currentThrowsCount > nextThrowsCount) { + next + } else { + val currentParameterCount = current.parameters().size + val nextParameterCount = next.parameters().size + if (currentParameterCount <= nextParameterCount) { + current + } else + next + } + } + + fun generateInheritedStubs(filterEmit: Predicate<Item>, filterReference: Predicate<Item>) { + packages.allClasses().forEach { + if (filterEmit.test(it)) { + generateInheritedStubs(it, filterEmit, filterReference) + } + } + } + + private fun generateInheritedStubs(cls: ClassItem, filterEmit: Predicate<Item>, filterReference: Predicate<Item>) { + if (!cls.isClass()) return + if (cls.superClass() == null) return + val superClasses: Sequence<ClassItem> = generateSequence(cls.superClass()) { it.superClass() } + val hiddenSuperClasses: Sequence<ClassItem> = + superClasses.filter { !filterReference.test(it) && !it.isJavaLangObject() } + + if (hiddenSuperClasses.none()) { // not missing any implementation methods + return + } + + addInheritedStubsFrom(cls, hiddenSuperClasses, superClasses, filterEmit, filterReference) + addInheritedInterfacesFrom(cls, hiddenSuperClasses, filterReference) + + } + + private fun addInheritedInterfacesFrom( + cls: ClassItem, + hiddenSuperClasses: Sequence<ClassItem>, + filterReference: Predicate<Item> + ) { + var interfaceTypes: MutableList<TypeItem>? = null + var interfaceTypeClasses: MutableList<ClassItem>? = null + for (hiddenSuperClass in hiddenSuperClasses) { + for (hiddenInterface in hiddenSuperClass.interfaceTypes()) { + val hiddenInterfaceClass = hiddenInterface.asClass() + if (filterReference.test(hiddenInterfaceClass ?: continue)) { + if (interfaceTypes == null) { + interfaceTypes = cls.interfaceTypes().toMutableList() + interfaceTypeClasses = + interfaceTypes.asSequence().map { it.asClass() }.filterNotNull().toMutableList() + if (cls.isInterface()) { + cls.superClass()?.let { interfaceTypeClasses.add(it) } + } + cls.setInterfaceTypes(interfaceTypes) + } + if (interfaceTypeClasses!!.any { it == hiddenInterfaceClass }) { + continue + } + + interfaceTypeClasses.add(hiddenInterfaceClass) + + if (hiddenInterfaceClass.hasTypeVariables()) { + val mapping = cls.mapTypeVariables(hiddenSuperClass) + if (mapping.isNotEmpty()) { + val mappedType: TypeItem = hiddenInterface.convertType(mapping, cls) + interfaceTypes.add(mappedType) + continue + } + } + + interfaceTypes.add(hiddenInterface) + } + } + } + } + + private fun addInheritedStubsFrom( + cls: ClassItem, + hiddenSuperClasses: Sequence<ClassItem>, + superClasses: Sequence<ClassItem>, + filterEmit: Predicate<Item>, filterReference: Predicate<Item> + ) { + + // Also generate stubs for any methods we would have inherited from abstract parents + // All methods from super classes that (1) aren't overridden in this class already, and + // (2) are overriding some method that is in a public interface accessible from this class. + val interfaces: Set<TypeItem> = cls.allInterfaceTypes(filterReference).asSequence().toSet() + + // Note that we can't just call method.superMethods() to and see whether any of their containing + // classes are among our target APIs because it's possible that the super class doesn't actually + // implement the interface, but still provides a matching signature for the interface. + // Instead we'll look through all of our interface methods and look for potential overrides + val interfaceNames = mutableMapOf<String, MutableList<MethodItem>>() + for (interfaceType in interfaces) { + val interfaceClass = interfaceType.asClass() ?: continue + for (method in interfaceClass.methods()) { + val name = method.name() + val list = interfaceNames[name] ?: run { + val list = ArrayList<MethodItem>() + interfaceNames[name] = list + list + } + list.add(method) + } + } + + // Also add in any abstract methods from public super classes + val publicSuperClasses = superClasses.filter { filterEmit.test(it) && !it.isJavaLangObject() } + for (superClass in publicSuperClasses) { + 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). + // map from method name to super methods overriding our interfaces + val map = HashMap<String, MutableList<MethodItem>>() + + for (superClass in hiddenSuperClasses) { + for (method in superClass.methods()) { + val modifiers = method.modifiers + if (!modifiers.isPrivate() && !modifiers.isAbstract()) { + val name = method.name() + val candidates = interfaceNames[name] ?: continue + val parameterCount = method.parameters().size + for (superMethod in candidates) { + if (parameterCount != superMethod.parameters().count()) { + continue + } + if (method.matches(superMethod)) { + val list = map[name] ?: run { + val newList = ArrayList<MethodItem>() + map[name] = newList + newList + } + list.add(method) + break + } + } + } + } + } + + // Remove any methods that are overriding any of our existing methods + for (method in cls.methods()) { + val name = method.name() + val candidates = map[name] ?: continue + val iterator = candidates.listIterator() + while (iterator.hasNext()) { + val inheritedMethod = iterator.next() + if (method.matches(inheritedMethod)) { + iterator.remove() + } + } + } + + // Next remove any overrides among the remaining super methods (e.g. one method from a hidden parent is + // overriding another method from a more distant hidden parent). + map.values.forEach { methods -> + if (methods.size >= 2) { + for (candidate in ArrayList(methods)) { + methods.removeAll(candidate.superMethods()) + } + } + } + + // We're now left with concrete methods in hidden parents that are implementing methods in public + // interfaces that are listed in this class. Create stubs for them: + map.values.flatten().forEach { + val method = cls.createMethod(it) + method.documentation = "// Inlined stub from hidden parent class ${it.containingClass().qualifiedName()}\n" + + method.documentation + method.inheritedInterfaceMethod = true + cls.addMethod(method) + } + } + + /** Hide packages explicitly listed in [Options.hidePackages] */ + private fun hidePackages() { + for (pkgName in options.hidePackages) { + val pkg = codebase.findPackage(pkgName) ?: continue + pkg.hidden = true + pkg.included = false // because included has already been initialized + } + } + + /** Apply emit filters listed in [Options.skipEmitPackages] */ + private fun skipEmitPackages() { + for (pkgName in options.skipEmitPackages) { + val pkg = codebase.findPackage(pkgName) ?: continue + pkg.emit = false + } + } + + /** Merge in external data from all configured sources */ + fun mergeExternalAnnotations() { + val mergeAnnotations = options.mergeAnnotations + if (!mergeAnnotations.isEmpty()) { + AnnotationsMerger(codebase, options.apiFilter).merge(mergeAnnotations) + } + } + + /** + * Propagate the hidden flag down into individual elements -- if a class is hidden, then the methods and fields + * are hidden etc + */ + private fun propagateHiddenRemovedAndDocOnly(includingFields: Boolean) { + packages.accept(object : ItemVisitor(visitConstructorsAsMethods = true, nestInnerClasses = true) { + override fun visitItem(item: Item) { + if (item.modifiers.hasShowAnnotation()) { + item.hidden = false + } else if (item.modifiers.hasHideAnnotations()) { + item.hidden = true + } + } + + override fun visitPackage(pkg: PackageItem) { + val containingPackage = pkg.containingPackage() + if (containingPackage != null) { + if (containingPackage.hidden) { + pkg.hidden = true + } + if (containingPackage.docOnly) { + pkg.docOnly = true + } + } + } + + override fun visitClass(cls: ClassItem) { + val containingClass = cls.containingClass() + if (containingClass != null) { + if (containingClass.hidden) { + cls.hidden = true + } + if (containingClass.docOnly) { + cls.docOnly = true + } + if (containingClass.removed) { + cls.removed = true + } + } else { + val containingPackage = cls.containingPackage() + if (containingPackage.hidden && !containingPackage.isDefault) { + cls.hidden = true + } + if (containingPackage.docOnly && !containingPackage.isDefault) { + cls.docOnly = true + } + if (containingPackage.removed && !cls.modifiers.hasShowAnnotation()) { + cls.removed = true + } + } + } + + override fun visitMethod(method: MethodItem) { + val containingClass = method.containingClass() + if (containingClass.hidden) { + method.hidden = true + } + if (containingClass.docOnly) { + method.docOnly = true + } + if (containingClass.removed) { + method.removed = true + } + } + + override fun visitField(field: FieldItem) { + val containingClass = field.containingClass() + /* We don't always propagate field visibility down to the fields + because we sometimes move fields around, and in that + case we don't want to carry forward the "hidden" attribute + from the field that wasn't marked on the field but its + container interface. + */ + if (includingFields && containingClass.hidden) { + field.hidden = true + } + if (containingClass.docOnly) { + field.docOnly = true + } + if (containingClass.removed) { + field.removed = true + } + } + }) + } + + private fun applyApiFilter() { + options.apiFilter?.let { filter -> + packages.accept(object : VisibleItemVisitor() { + + override fun visitPackage(pkg: PackageItem) { + if (!filter.hasPackage(pkg.qualifiedName())) { + pkg.included = false + } + } + + override fun visitClass(cls: ClassItem) { + if (!filter.hasClass(cls.qualifiedName())) { + cls.included = false + } + } + + override fun visitMethod(method: MethodItem) { + if (!filter.hasMethod( + method.containingClass().qualifiedName(), method.name(), + method.formatParameters() + ) + ) { + method.included = false + } + } + + override fun visitField(field: FieldItem) { + if (!filter.hasField(field.containingClass().qualifiedName(), field.name())) { + field.included = false + } + } + }) + } + } + + private fun checkHiddenTypes() { + packages.accept(object : ApiVisitor(codebase, visitConstructorsAsMethods = false) { + override fun visitMethod(method: MethodItem) { + checkType(method, method.returnType() ?: return) // constructors don't have + } + + override fun visitField(field: FieldItem) { + checkType(field, field.type()) + } + + override fun visitParameter(parameter: ParameterItem) { + checkType(parameter, parameter.type()) + } + + private fun checkType(item: Item, type: TypeItem) { + if (type.primitive) { + return + } + + val cls = type.asClass() + + // Don't flag type parameters like T + if (cls?.isTypeParameter == true) { + return + } + + // class may be null for things like array types and ellipsis types, + // but iterating through the type argument classes below will find and + // check the component class + if (cls != null && !filterReference.test(cls) && !cls.isFromClassPath()) { + reporter.report( + Errors.HIDDEN_TYPE_PARAMETER, item, + "${item.toString().capitalize()} references hidden type $type." + ) + } + + type.typeArgumentClasses() + .filter { it != cls } + .forEach { checkType(item, it) } + } + + private fun checkType(item: Item, cls: ClassItem) { + if (!filterReference.test(cls)) { + if (!cls.isFromClassPath()) { + reporter.report( + Errors.HIDDEN_TYPE_PARAMETER, item, + "${item.toString().capitalize()} references hidden type $cls." + ) + } + } else { + cls.typeArgumentClasses() + .filter { it != cls } + .forEach { checkType(item, it) } + } + } + }) + } + + private fun ensureSystemServicesProtectedWithPermission() { + if (options.showAnnotations.contains("android.annotation.SystemApi") && options.manifest != null) { + // Look for Android @SystemApi exposed outside the normal SDK; we require + // that they're protected with a system permission. + + packages.accept(object : ApiVisitor(codebase) { + override fun visitClass(cls: ClassItem) { + // This class is a system service if it's annotated with @SystemService, + // or if it's android.content.pm.PackageManager + if (cls.modifiers.isAnnotatedWith("android.annotation.SystemService") || + cls.qualifiedName() == "android.content.pm.PackageManager" + ) { + // Check permissions on system services + for (method in cls.filteredMethods(filterEmit)) { + checkSystemPermissions(method) + } + } + } + }) + } + } + + private fun checkSystemPermissions(method: MethodItem) { + if (method.isImplicitConstructor()) { // Don't warn on non-source elements like implicit default constructors + return + } + + val annotation = method.modifiers.findAnnotation("android.annotation.RequiresPermission") + var hasAnnotation = false + + if (annotation != null) { + hasAnnotation = true + for (attribute in annotation.attributes()) { + var values: List<AnnotationAttributeValue>? = null + var any = false + when (attribute.name) { + "value", "allOf" -> { + values = attribute.leafValues() + } + "anyOf" -> { + any = true + values = attribute.leafValues() + } + } + + values ?: continue + + val system = ArrayList<String>() + val nonSystem = ArrayList<String>() + val missing = ArrayList<String>() + for (value in values) { + val perm = (value.value() ?: value.toSource()).toString() + val level = codebase.getPermissionLevel(perm) + if (level == null) { + if (any) { + missing.add(perm) + continue + } + + reporter.report( + Errors.REMOVED_FIELD, method, + "Permission '$perm' is not defined by manifest ${codebase.manifest}." + ) + continue + } + if (level.contains("normal") || level.contains("dangerous") + || level.contains("ephemeral") + ) { + nonSystem.add(perm) + } else { + system.add(perm) + } + } + if (any && missing.size == values.size) { + reporter.report( + Errors.REMOVED_FIELD, method, + "None of the permissions ${missing.joinToString()} are defined by manifest " + + "${codebase.manifest}." + ) + } + + if (system.isEmpty() && nonSystem.isEmpty()) { + hasAnnotation = false + } else if (any && !nonSystem.isEmpty() || !any && system.isEmpty()) { + reporter.report( + Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() + + "' must be protected with a system permission; it currently" + + " allows non-system callers holding " + nonSystem.toString() + ) + } + } + } + + if (!hasAnnotation) { + reporter.report( + Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() + + "' must be protected with a system permission." + ) + } + } + + fun performChecks() { + if (codebase.trustedApi()) { + // The codebase is already an API; no consistency checks to be performed + return + } + + // TODO for performance: Single iteration over the whole API surface! + ensureSystemServicesProtectedWithPermission() + checkHiddenTypes() + + packages.accept(object : ApiVisitor(codebase) { + override fun visitItem(item: Item) { + // TODO: Check annotations and also mark removed/hidden based on annotations + if (item.deprecated && !item.documentation.contains("@deprecated")) { + reporter.report( + Errors.DEPRECATION_MISMATCH, item, + "${item.toString().capitalize()}: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match" + ) + } + // TODO: Check opposite (doc tag but no annotation) + // TODO: Other checks + } + }) + } + + fun handleStripping() { + // TODO: Switch to visitor iteration + //val stubPackages = options.stubPackages + val stubImportPackages = options.stubImportPackages + handleStripping(stubImportPackages) + } + + private fun handleStripping(stubImportPackages: Set<String>) { + val notStrippable = HashSet<ClassItem>(5000) + + // If a class is public or protected, not hidden, not imported and marked as included, + // then we can't strip it + val allTopLevelClasses = codebase.getPackages().allTopLevelClasses().toList() + allTopLevelClasses + .filter { it.checkLevel() && it.emit && !it.hidden() } + .forEach { + cantStripThis(it, notStrippable, stubImportPackages) + } + + // complain about anything that looks includeable but is not supposed to + // be written, e.g. hidden things + for (cl in notStrippable) { + if (!cl.isHiddenOrRemoved()) { + for (m in cl.methods()) { + if (!m.checkLevel()) { + continue + } + if (m.isHiddenOrRemoved()) { + reporter.report( + Errors.UNAVAILABLE_SYMBOL, m, + "Reference to unavailable method " + m.name() + ) + } else if (m.deprecated) { + // don't bother reporting deprecated methods + // unless they are public + reporter.report( + Errors.DEPRECATED, m, "Method " + cl.qualifiedName() + "." + + m.name() + " is deprecated" + ) + } + + val returnType = m.returnType() + var hiddenClass = findHiddenClasses(returnType, stubImportPackages) + if (hiddenClass != null && !hiddenClass.isFromClassPath()) { + if (hiddenClass.qualifiedName() == returnType?.asClass()?.qualifiedName()) { + // Return type is hidden + reporter.report( + Errors.UNAVAILABLE_SYMBOL, m, + "Method ${cl.qualifiedName()}.${m.name()} returns unavailable " + + "type ${hiddenClass.simpleName()}" + ) + } else { + // Return type contains a generic parameter + reporter.report( + Errors.HIDDEN_TYPE_PARAMETER, m, + "Method ${cl.qualifiedName()}.${m.name()} returns unavailable " + + "type ${hiddenClass.simpleName()} as a type parameter" + ) + } + } + + for (p in m.parameters()) { + val t = p.type() + if (!t.primitive) { + hiddenClass = findHiddenClasses(t, stubImportPackages) + if (hiddenClass != null && !hiddenClass.isFromClassPath()) { + if (hiddenClass.qualifiedName() == t.asClass()?.qualifiedName()) { + // Parameter type is hidden + reporter.report( + Errors.UNAVAILABLE_SYMBOL, m, + "Parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()" + ) + } else { + // Parameter type contains a generic parameter + reporter.report( + Errors.HIDDEN_TYPE_PARAMETER, m, + "Parameter uses type parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()" + ) + } + } + } + } + } + } else if (cl.deprecated) { + // not hidden, but deprecated + reporter.report(Errors.DEPRECATED, cl, "Class ${cl.qualifiedName()} is deprecated") + } + } + } + + private fun cantStripThis( + cl: ClassItem, + notStrippable: MutableSet<ClassItem>, + stubImportPackages: Set<String>? + ) { + if (stubImportPackages != null && stubImportPackages.contains(cl.containingPackage().qualifiedName())) { + // if the package is imported then it does not need stubbing. + return + } + + if (cl.isFromClassPath()) { + return + } + + if (!notStrippable.add(cl)) { + // slight optimization: if it already contains cl, it already contains + // all of cl's parents + return + } + + // cant strip any public fields or their generics + for (field in cl.fields()) { + if (!field.checkLevel()) { + continue + } + val fieldType = field.type() + if (!fieldType.primitive) { + val typeClass = fieldType.asClass() + if (typeClass != null) { + cantStripThis( + typeClass, notStrippable, stubImportPackages + ) + } + for (cls in fieldType.typeArgumentClasses()) { + cantStripThis( + cls, notStrippable, stubImportPackages + ) + } + } + } + // cant strip any of the type's generics + for (cls in cl.typeArgumentClasses()) { + cantStripThis( + cls, notStrippable, stubImportPackages + ) + } + // cant strip any of the annotation elements + // cantStripThis(cl.annotationElements(), notStrippable); + // take care of methods + cantStripThis(cl.methods(), notStrippable, stubImportPackages) + cantStripThis(cl.constructors(), notStrippable, stubImportPackages) + // blow the outer class open if this is an inner class + val containingClass = cl.containingClass() + if (containingClass != null) { + cantStripThis( + containingClass, notStrippable, stubImportPackages + ) + } + // blow open super class and interfaces + val supr = cl.superClass() + if (supr != null) { + if (supr.isHiddenOrRemoved()) { + // cl is a public class declared as extending a hidden superclass. + // this is not a desired practice but it's happened, so we deal + // with it by finding the first super class which passes checklevel for purposes of + // generating the doc & stub information, and proceeding normally. + val publicSuper = cl.publicSuperClass() + // TODO: Initialize and pass super type too (in case generics are involved) + cl.setSuperClass(publicSuper) + if (!supr.isFromClassPath()) { + reporter.report( + Errors.HIDDEN_SUPERCLASS, cl, "Public class " + cl.qualifiedName() + + " stripped of unavailable superclass " + supr.qualifiedName() + ) + } + } else { + cantStripThis( + supr, notStrippable, stubImportPackages + ) + if (supr.isPrivate && !supr.isFromClassPath()) { + reporter.report( + Errors.PRIVATE_SUPERCLASS, cl, "Public class " + + cl.qualifiedName() + " extends private class " + supr.qualifiedName() + ) + } + } + } + } + + private fun cantStripThis( + methods: List<MethodItem>, notStrippable: MutableSet<ClassItem>, + stubImportPackages: Set<String>? + ) { + // for each method, blow open the parameters, throws and return types. also blow open their + // generics + for (method in methods) { + if (!method.checkLevel()) { + continue + } + for (typeParameterClass in method.typeArgumentClasses()) { + cantStripThis( + typeParameterClass, notStrippable, + stubImportPackages + ) + } + for (parameter in method.parameters()) { + for (parameterTypeClass in parameter.type().typeArgumentClasses()) { + cantStripThis( + parameterTypeClass, notStrippable, stubImportPackages + ) + for (tcl in parameter.type().typeArgumentClasses()) { + if (tcl.isHiddenOrRemoved()) { + reporter.report( + Errors.UNAVAILABLE_SYMBOL, method, + "Parameter of hidden type ${tcl.fullName()}" + + "in ${method.containingClass().qualifiedName()}.${method.name()}()" + ) + } else { + cantStripThis( + tcl, notStrippable, + stubImportPackages + ) + } + } + } + } + for (thrown in method.throwsTypes()) { + cantStripThis( + thrown, notStrippable, stubImportPackages + ) + } + val returnType = method.returnType() + if (returnType != null && !returnType.primitive) { + val returnTypeClass = returnType.asClass() + if (returnTypeClass != null) { + cantStripThis( + returnTypeClass, notStrippable, + stubImportPackages + ) + for (tyItem in returnType.typeArgumentClasses()) { + cantStripThis( + tyItem, notStrippable, + stubImportPackages + ) + } + } + } + } + } + + /** + * Find references to hidden classes. + * + * This finds hidden classes that are used by public parts of the API in order to ensure the + * API is self consistent and does not reference classes that are not included in + * the stubs. Any such references cause an error to be reported. + * + * A reference to an imported class is not treated as an error, even though imported classes + * are hidden from the stub generation. That is because imported classes are, by definition, + * excluded from the set of classes for which stubs are required. + * + * @param ti the type information to examine for references to hidden classes. + * @param stubImportPackages the possibly null set of imported package names. + * @return a reference to a hidden class or null if there are none + */ + private fun findHiddenClasses(ti: TypeItem?, stubImportPackages: Set<String>?): ClassItem? { + ti ?: return null + val ci = ti.asClass() ?: return null + return findHiddenClasses(ci, stubImportPackages) + } + + private fun findHiddenClasses(ci: ClassItem, stubImportPackages: Set<String>?): ClassItem? { + if (stubImportPackages != null && stubImportPackages.contains(ci.containingPackage().qualifiedName())) { + return null + } + if (ci.isHiddenOrRemoved()) return ci + for (tii in ci.toType().typeArgumentClasses()) { + // Avoid infinite recursion in the case of Foo<T extends Foo> + if (tii != ci) { + val hiddenClass = findHiddenClasses(tii, stubImportPackages) + if (hiddenClass != null) return hiddenClass + } + } + return null + } +} diff --git a/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt new file mode 100644 index 0000000..d906578 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2017 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.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.intellij.util.containers.Stack +import java.util.Comparator +import java.util.function.Predicate + +/** + * Visitor which visits all items in two matching codebases and + * matches up the items and invokes [compare] on each pair, or + * [added] or [removed] when items are not matched + */ +open class ComparisonVisitor { + open fun compare(old: Item, new: Item) {} + open fun added(item: Item) {} + open fun removed(item: Item) {} + + open fun compare(old: PackageItem, new: PackageItem) {} + open fun compare(old: ClassItem, new: ClassItem) {} + open fun compare(old: MethodItem, new: MethodItem) {} + open fun compare(old: FieldItem, new: FieldItem) {} + open fun compare(old: ParameterItem, new: ParameterItem) {} + + open fun added(item: PackageItem) {} + open fun added(item: ClassItem) {} + open fun added(item: MethodItem) {} + open fun added(item: FieldItem) {} + open fun added(item: ParameterItem) {} + + open fun removed(item: PackageItem) {} + open fun removed(item: ClassItem) {} + open fun removed(item: MethodItem) {} + open fun removed(item: FieldItem) {} + open fun removed(item: ParameterItem) {} +} + +class CodebaseComparator { + /** + * Visits this codebase and compares it with another codebase, informing the visitors about + * the correlations and differences that it finds + */ + fun compare(visitor: ComparisonVisitor, old: Codebase, new: Codebase, filter: Predicate<Item>? = null) { + // Algorithm: build up two trees (by nesting level); then visit the + // two trees + val oldTree = createTree(old, filter) + val newTree = createTree(new, filter) + compare(visitor, oldTree, newTree) + } + + private fun compare(visitor: ComparisonVisitor, oldList: List<ItemTree>, newList: List<ItemTree>) { + var index1 = 0 + var index2 = 0 + val length1 = oldList.size + val length2 = newList.size + + while (true) { + if (index1 < length1) { + if (index2 < length2) { + // Compare the items + val oldTree = oldList[index1] + val newTree = newList[index2] + val old = oldTree.item() + val new = newTree.item() + val compare = compare(old, new) + when { + compare > 0 -> { + index2++ + visitAdded(visitor, new) + } + compare < 0 -> { + index1++ + visitRemoved(visitor, old) + } + else -> { + visitCompare(visitor, old, new) + + // Compare the children (recurse) + compare(visitor, oldTree.children, newTree.children) + + index1++ + index2++ + } + } + + } else { + // All the remaining items in oldList have been deleted + while (index1 < length1) { + visitRemoved(visitor, oldList[index1++].item()) + } + } + } else if (index2 < length2) { + // All the remaining items in newList have been added + while (index2 < length2) { + visitAdded(visitor, newList[index2++].item()) + } + } else { + break + } + } + } + + private fun visitAdded(visitor: ComparisonVisitor, item: Item) { + visitor.added(item) + + when (item) { + is PackageItem -> visitor.added(item) + is ClassItem -> visitor.added(item) + is MethodItem -> visitor.added(item) + is FieldItem -> visitor.added(item) + is ParameterItem -> visitor.added(item) + } + } + + private fun visitRemoved(visitor: ComparisonVisitor, item: Item) { + visitor.added(item) + + when (item) { + is PackageItem -> visitor.removed(item) + is ClassItem -> visitor.removed(item) + is MethodItem -> visitor.removed(item) + is FieldItem -> visitor.removed(item) + is ParameterItem -> visitor.removed(item) + } + } + + private fun visitCompare(visitor: ComparisonVisitor, old: Item, new: Item) { + visitor.compare(old, new) + + when (old) { + is PackageItem -> visitor.compare(old, new as PackageItem) + is ClassItem -> visitor.compare(old, new as ClassItem) + is MethodItem -> visitor.compare(old, new as MethodItem) + is FieldItem -> visitor.compare(old, new as FieldItem) + is ParameterItem -> visitor.compare(old, new as ParameterItem) + } + } + + private fun compare(item1: Item, item2: Item): Int = comparator.compare(item1, item2) + + companion object { + /** Sorting rank for types */ + private fun typeRank(item: Item): Int { + return when (item) { + is PackageItem -> 0 + is MethodItem -> if (item.isConstructor()) 1 else 2 + is FieldItem -> 3 + is ClassItem -> 4 + is ParameterItem -> 5 + is AnnotationItem -> 6 + else -> 7 + } + } + + val comparator: Comparator<Item> = Comparator { item1, item2 -> + val typeSort = typeRank(item1) - typeRank(item2) + when { + typeSort != 0 -> typeSort + item1 == item2 -> 0 + else -> when (item1) { + is PackageItem -> { + item1.qualifiedName().compareTo((item2 as PackageItem).qualifiedName()) + } + is ClassItem -> { + item1.qualifiedName().compareTo((item2 as ClassItem).qualifiedName()) + } + is MethodItem -> { + val delta = item1.name().compareTo((item2 as MethodItem).name()) + // TODO: Sort by signatures/parameters + delta + } + is FieldItem -> { + item1.name().compareTo((item2 as FieldItem).name()) + } + is ParameterItem -> { + item1.parameterIndex.compareTo((item2 as ParameterItem).parameterIndex) + } + is AnnotationItem -> { + (item1.qualifiedName() ?: "").compareTo((item2 as AnnotationItem).qualifiedName() ?: "") + } + else -> { + error("Unexpected item type ${item1.javaClass}") + } + } + } + } + + val treeComparator: Comparator<ItemTree> = Comparator { item1, item2 -> + comparator.compare(item1.item, item2.item()) + } + } + + private fun ensureSorted(items: MutableList<ItemTree>) { + items.sortWith(treeComparator) + for (item in items) { + ensureSorted(item) + } + } + + private fun ensureSorted(item: ItemTree) { + item.children.sortWith(treeComparator) + for (child in item.children) { + ensureSorted(child) + } + } + + private fun createTree(codebase: Codebase, filter: Predicate<Item>? = null): List<ItemTree> { + // TODO: Make sure the items are sorted! + val stack = Stack<ItemTree>() + val root = ItemTree(null) + stack.push(root) + + val predicate = filter ?: Predicate { true } + // TODO: Skip empty packages + codebase.accept(object : ApiVisitor( + nestInnerClasses = true, + inlineInheritedFields = true, + filterEmit = predicate, + filterReference = predicate + ) { + override fun visitItem(item: Item) { + val node = ItemTree(item) + val parent = stack.peek() + parent.children += node + + stack.push(node) + } + + override fun afterVisitItem(item: Item) { + stack.pop() + } + }) + + ensureSorted(root.children) + return root.children + } + + data class ItemTree(val item: Item?) : Comparable<ItemTree> { + val children: MutableList<ItemTree> = mutableListOf() + fun item(): Item = item!! // Only the root note can be null, and this method should never be called on it + + override fun compareTo(other: ItemTree): Int { + return comparator.compare(item(), other.item()) + } + + override fun toString(): String { + return item.toString() + } + + fun prettyPrint(): String { + val sb = StringBuilder(1000) + prettyPrint(sb, 0) + return sb.toString() + } + + private fun prettyPrint(sb: StringBuilder, depth: Int) { + for (i in 0 until depth) { + sb.append(" ") + } + sb.append(toString()) + sb.append('\n') + for (child in children) { + child.prettyPrint(sb, depth + 1) + } + } + + companion object { + fun prettyPrint(list: List<ItemTree>): String { + val sb = StringBuilder(1000) + for (child in list) { + child.prettyPrint(sb, 0) + } + return sb.toString() + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/Compatibility.kt b/src/main/java/com/android/tools/metalava/Compatibility.kt new file mode 100644 index 0000000..7abc4de --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Compatibility.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 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 COMPAT_MODE_BY_DEFAULT = true + +/** + * The old API generator code had a number of quirks. Initially we want to simulate these + * quirks to produce compatible signature files and APIs, but we want to track what these quirks + * are and be able to turn them off eventually. This class offers more fine grained control + * of these compatibility behaviors such that we can enable/disable them selectively + */ +var compatibility: Compatibility = Compatibility() + +class Compatibility( + /** Whether compatibility is generally on */ + val compat: Boolean = COMPAT_MODE_BY_DEFAULT +) { + + /** Whether to inline fields from implemented interfaces into concrete classes */ + var inlineInterfaceFields: Boolean = compat + + /** In signature files, use "implements" instead of "extends" for the super class of + * an interface */ + var extendsForInterfaceSuperClass: Boolean = compat + + /** In signature files, refer to annotations as an "abstract class" instead of an "@interface" + * and implementing this interface: java.lang.annotation.Annotation */ + var classForAnnotations: Boolean = compat + + /** Add in explicit `valueOf` and `values` methods into annotation classes */ + var defaultAnnotationMethods: Boolean = compat + + /** In signature files, refer to enums as "class" instead of "enum" */ + var classForEnums: Boolean = compat + + /** Whether to use a nonstandard, compatibility modifier order instead of the Java canonical order. + * ("deprecated" isn't a real modifier, so in "standard" mode it's listed first, as if it was the + * `@Deprecated` annotation before the modifier list */ + var nonstandardModifierOrder: Boolean = compat + + /** In signature files, skip the native modifier from the modifier lists */ + var skipNativeModifier: Boolean = nonstandardModifierOrder + + /** In signature files, skip the strictfp modifier from the modifier lists */ + var skipStrictFpModifier: Boolean = nonstandardModifierOrder + + /** Whether to include instance methods in annotation classes for the annotation properties */ + var skipAnnotationInstanceMethods: Boolean = compat + + /** Include spaces after commas in type strings */ + var spacesAfterCommas: Boolean = compat + + /** + * In signature files, whether interfaces should also be described as "abstract" + */ + var abstractInInterfaces: Boolean = compat + + /** + * In signature files, whether annotation types should also be described as "abstract" + */ + var abstractInAnnotations: Boolean = compat + + /** + * In signature files, whether interfaces can be listed as final + */ + var finalInInterfaces: Boolean = compat + + /** + * In this signature + * public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X { + * doclava1 would treat this as "throws Throwable" instead of "throws X". This variable turns on + * this compat behavior. + * */ + var useErasureInThrows: Boolean = compat + + /** + * Include a single space in front of package private classes with no other modifiers + * (this doesn't align well, but is supported to make the output 100% identical to the + * doclava1 format + */ + var extraSpaceForEmptyModifiers: Boolean = compat + + /** Format `Map<K,V>` as `Map<K, V>` */ + var spaceAfterCommaInTypes: Boolean = compat + + /** + * Doclava1 sorts classes/interfaces by class name instead of qualified name + */ + var sortClassesBySimpleName: Boolean = compat + + /** + * Doclava1 omits type parameters in interfaces (in signature files, not in stubs) + */ + var omitTypeParametersInInterfaces: Boolean = compat + + /** + * Doclava1 sorted the methods like this: + * + * public final class RoundingMode extends java.lang.Enum { + * method public static java.math.RoundingMode valueOf(java.lang.String); + * method public static java.math.RoundingMode valueOf(int); + * ... + * + * Note how the two valueOf methods are out of order. With this compatibility mode, + * we try to perform the same sorting. + */ + var sortEnumValueOfMethodFirst: Boolean = compat + + /** + * Whether packages should be treated as recursive for documentation. In other words, + * if a directory has a `packages.html` file containing a `@hide` comment, then + * all "sub" packages (directories below this one) will also inherit the same comment. + * Java packages aren't supposed to work that way, but doclava does. + */ + var inheritPackageDocs: Boolean = compat + + /** Force methods named "values" in enums to be marked final. This was done by + * doclava1 with this comment: + * + * Explicitly coerce 'final' state of Java6-compiled enum values() method, + * to match the Java5-emitted base API description. + * + **/ + var forceFinalInEnumValueMethods: Boolean = compat + + /** Whether signature files and stubs should contain annotations */ + var annotationsInSignatures: Boolean = !compat + + /** Emit errors in the old API diff format */ + var oldErrorOutputFormat: Boolean = false + + /** + * When a public class implementing a public interface inherits the implementation + * of a method in that interface from a hidden super class, the method must be + * included in the stubs etc (since otherwise subclasses would believe they need + * to implement that method and can't just inherit it). However, doclava1 does not + * list these methods. This flag controls this compatibility behavior. + */ + var skipInheritedInterfaceMethods: Boolean = compat + + /** + * Whether to include parameter names in the signature file + */ + val parameterNames: Boolean = true + + // 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/CompatibilityCheck.kt b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt new file mode 100644 index 0000000..d935438 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017 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.NullnessMigration.Companion.findNullnessAnnotation +import com.android.tools.metalava.NullnessMigration.Companion.isNullable +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.doclava1.Errors.Error +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.ParameterItem + +/** + * Compares the current API with a previous version and makes sure + * the changes are compatible. For example, you can make a previously + * nullable parameter non null, but not vice versa. + * + * TODO: Only allow nullness changes on final classes! + */ +class CompatibilityCheck : ComparisonVisitor() { + override fun compare(old: Item, new: Item) { + // Should not remove nullness information + // Can't change information incompatibly + val oldNullnessAnnotation = findNullnessAnnotation(old) + if (oldNullnessAnnotation != null) { + val newNullnessAnnotation = findNullnessAnnotation(new) + if (newNullnessAnnotation == null) { + val name = AnnotationItem.simpleName(oldNullnessAnnotation) + reporter.report( + Errors.INVALID_NULL_CONVERSION, new, + "Attempted to remove $name annotation from ${describe(new)}" + ) + } else { + val oldNullable = isNullable(old) + val newNullable = isNullable(new) + if (oldNullable != newNullable) { + // You can change a parameter from nonnull to nullable + // You can change a method from nullable to nonnull + // You cannot change a parameter from nullable to nonnull + // You cannot change a method from nonnull to nullable + if (oldNullable && old is ParameterItem) { + reporter.report( + Errors.INVALID_NULL_CONVERSION, new, + "Attempted to change parameter from @Nullable to @NonNull: " + + "incompatible change for ${describe(new)}" + ) + } else if (!oldNullable && old is MethodItem) { + reporter.report( + Errors.INVALID_NULL_CONVERSION, new, + "Attempted to change method return from @NonNull to @Nullable: " + + "incompatible change for ${describe(new)}" + ) + } + } + } + } + } + + override fun compare(old: ParameterItem, new: ParameterItem) { + val prevName = old.publicName() ?: return + val newName = new.publicName() + if (newName == null) { + reporter.report( + Errors.PARAMETER_NAME_CHANGE, new, + "Attempted to remove parameter name from ${describe(new)} in ${describe(new.containingMethod())}" + ) + } else if (newName != prevName) { + reporter.report( + Errors.PARAMETER_NAME_CHANGE, new, + "Attempted to change parameter name from $prevName to $newName in ${describe(new.containingMethod())}" + ) + } + } + + override fun compare(old: ClassItem, new: ClassItem) { + if (old.isInterface() != new.isInterface()) { + reporter.report( + Errors.CHANGED_CLASS, new, "Class " + new.qualifiedName() + + " changed class/interface declaration" + ) + } + } + + private fun handleAdded(error: Error, item: Item) { + if (item is MethodItem) { + // *Overriding* methods from super classes that are outside the + // API is OK (e.g. overriding toString() from java.lang.Object) + val superMethods = item.superMethods() + for (superMethod in superMethods) { + if (superMethod.isFromClassPath()) { + return + } + } + } + + reporter.report(error, item, "Added ${describe(item)}") + } + + private fun handleRemoved(error: Error, item: Item) { + reporter.report(error, item, "Removed ${if (item.deprecated) "deprecated " else ""}${describe(item)}") + } + + override fun added(item: PackageItem) { + handleAdded(Errors.ADDED_PACKAGE, item) + } + + override fun added(item: ClassItem) { + val error = if (item.isInterface()) { + Errors.ADDED_INTERFACE + } else { + Errors.ADDED_CLASS + } + handleAdded(error, item) + } + + override fun added(item: MethodItem) { + handleAdded(Errors.ADDED_METHOD, item) + } + + override fun added(item: FieldItem) { + handleAdded(Errors.ADDED_FIELD, item) + } + + override fun removed(item: PackageItem) { + handleRemoved(Errors.REMOVED_PACKAGE, item) + } + + override fun removed(item: ClassItem) { + val error = when { + item.isInterface() -> Errors.REMOVED_INTERFACE + item.deprecated -> Errors.REMOVED_DEPRECATED_CLASS + else -> Errors.REMOVED_CLASS + } + handleRemoved(error, item) + } + + override fun removed(item: MethodItem) { + handleRemoved(Errors.REMOVED_METHOD, item) + } + + override fun removed(item: FieldItem) { + handleRemoved(Errors.REMOVED_FIELD, item) + } + + private fun describe(item: Item): String { + return when (item) { + is PackageItem -> "package ${item.qualifiedName()}" + is ClassItem -> "class ${item.qualifiedName()}" + is FieldItem -> "field ${item.containingClass().qualifiedName()}.${item.name()}" + is MethodItem -> "method ${item.containingClass().qualifiedName()}.${item.name()}" + is ParameterItem -> "parameter ${item.name()} in " + + "${item.containingMethod().containingClass().qualifiedName()}.${item.containingMethod().name()}" + else -> item.toString() + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/DexApiWriter.kt b/src/main/java/com/android/tools/metalava/DexApiWriter.kt new file mode 100644 index 0000000..15b4573 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/DexApiWriter.kt @@ -0,0 +1,73 @@ +/* + * 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.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import java.io.PrintWriter +import java.util.function.Predicate + +class DexApiWriter( + private val writer: PrintWriter, + filterEmit: Predicate<Item>, + filterReference: Predicate<Item> +) : ApiVisitor( + visitConstructorsAsMethods = true, + nestInnerClasses = false, + inlineInheritedFields = true, + filterEmit = filterEmit, + filterReference = filterReference +) { + override fun visitClass(cls: ClassItem) { + if (filterEmit.test(cls)) { + writer.print(cls.toType().internalName()) + writer.print("\n") + } + } + + override fun visitMethod(method: MethodItem) { + writer.print(method.containingClass().toType().internalName()) + writer.print("->") + writer.print(method.internalName()) + writer.print("(") + for (pi in method.parameters()) { + writer.print(pi.type().internalName()) + } + writer.print(")") + if (method.isConstructor()) { + writer.print("V") + } else { + val returnType = method.returnType() + writer.print(returnType?.internalName() ?: "V") + } + writer.print("\n") + } + + override fun visitField(field: FieldItem) { + val cls = field.containingClass() + + writer.print(cls.toType().internalName()) + writer.print("->") + writer.print(field.name()) + writer.print(":") + writer.print(field.type().internalName()) + writer.print("\n") + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt new file mode 100644 index 0000000..a70be9f --- /dev/null +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -0,0 +1,521 @@ +package com.android.tools.metalava + +import com.android.tools.lint.LintCliClient +import com.android.tools.lint.checks.ApiLookup +import com.android.tools.lint.helpers.DefaultJavaEvaluator +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.model.AnnotationAttributeValue +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +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.ParameterItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.android.tools.metalava.model.visitors.VisibleItemVisitor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import java.io.File +import java.util.HashMap +import java.util.regex.Pattern + +/** + * Walk over the API and apply tweaks to the documentation, such as + * - Looking for annotations and converting them to auxiliary tags + * that will be processed by the documentation tools later. + * - Reading lint's API database and inserting metadata into + * the documentation like api levels and deprecation levels. + * - Transferring docs from hidden super methods. + * - Performing tweaks for common documentation mistakes, such as + * ending the first sentence with ", e.g. " where javadoc will sadly + * see the ". " and think "aha, that's the end of the sentence!" + * (It works around this by replacing the space with .) + * This will also attempt to fix common typos (Andriod->Android etc). + */ +class DocAnalyzer( + /** The codebase to analyze */ + private val codebase: Codebase +) { + + /** Computes the visible part of the API from all the available code in the codebase */ + fun enhance() { + // Apply options for packages that should be hidden + documentsFromAnnotations() + + tweakGrammar() + + // TODO: + // addMinSdkVersionMetadata() + // addDeprecationMetadata() + // insertMissingDocFromHiddenSuperclasses() + } + + val mentionsNull: Pattern = Pattern.compile("\\bnull\\b") + + /** Hide packages explicitly listed in [Options.hidePackages] */ + private fun documentsFromAnnotations() { + // Note: Doclava1 inserts its own javadoc parameters into the documentation, + // which is then later processed by javadoc to insert actual descriptions. + // This indirection makes the actual descriptions of the annotations more + // configurable from a separate file -- but since this tool isn't hooked + // into javadoc anymore (and is going to be used by for example Dokka too) + // instead metalava will generate the descriptions directly in-line into the + // docs. + // + // This does mean that you have to update the metalava source code to update + // the docs -- but on the other hand all the other docs in the documentation + // set also requires updating framework source code, so this doesn't seem + // like an unreasonable burden. + + codebase.accept(object : ApiVisitor(codebase) { + override fun visitItem(item: Item) { + val annotations = item.modifiers.annotations() + if (annotations.isEmpty()) { + return + } + + for (annotation in annotations) { + handleAnnotation(annotation, item, depth = 0) + } + + /* Handled via @memberDoc/@classDoc on the annotations themselves right now. + That doesn't handle combinations of multiple thread annotations, but those + don't occur yet, right? + // Threading annotations: can't look at them one at a time; need to list them + // all together + if (item is ClassItem || item is MethodItem) { + val threads = findThreadAnnotations(annotations) + threads?.let { + val threadList = it.joinToString(separator = " or ") + + (if (it.size == 1) " thread" else " threads") + val doc = if (item is ClassItem) { + "All methods in this class must be invoked on the $threadList, unless otherwise noted" + } else { + assert(item is MethodItem) + "This method must be invoked on the $threadList" + } + appendDocumentation(doc, item, false) + } + } + */ + if (findThreadAnnotations(annotations).size > 1) { + reporter.warning( + item, "Found more than one threading annotation on $item; " + + "the auto-doc feature does not handle this correctly", + Errors.MULTIPLE_THREAD_ANNOTATIONS + ) + } + } + + private fun findThreadAnnotations(annotations: List<AnnotationItem>): List<String> { + 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 (result == null) { + result = mutableListOf() + } + val threadName = if (name.endsWith("UiThread")) { + "UI" + } else { + name.substring(name.lastIndexOf('.') + 1, name.length - "Thread".length) + } + result.add(threadName) + } + } + return result ?: emptyList() + } + + /** Fallback if field can't be resolved or if an inlined string value is used */ + private fun findPermissionField(codebase: Codebase, value: Any): FieldItem? { + val perm = value.toString() + val permClass = codebase.findClass("android.Manifest.permission") + permClass?.fields()?.filter { + it.initialValue(requireConstant = false)?.toString() == perm + }?.forEach { return it } + return null + } + + private fun handleAnnotation( + annotation: AnnotationItem, + item: Item, depth: Int + ) { + val name = annotation.qualifiedName() + if (name == null || name.startsWith("java.lang.")) { + // Ignore java.lang.Retention etc. + return + } + + // Some annotations include the documentation they want inlined into usage docs. + // Copy those here: + + if (annotation.isNullable() || annotation.isNonNull()) { + // Some docs already specifically talk about null policy; in that case, + // don't include the docs (since it may conflict with more specific conditions + // outlined in the docs). + if (item.documentation.contains("null") && + mentionsNull.matcher(item.documentation).find() + ) { + return + } + } + + when (item) { + is FieldItem -> { + addDoc(annotation, "memberDoc", item) + } + is MethodItem -> { + addDoc(annotation, "memberDoc", item) + addDoc(annotation, "returnDoc", item) + } + is ParameterItem -> { + addDoc(annotation, "paramDoc", item) + } + is ClassItem -> { + addDoc(annotation, "classDoc", item) + } + } + + // Document required permissions + if (item is MemberItem && name == "android.support.annotation.RequiresPermission") { + for (attribute in annotation.attributes()) { + var values: List<AnnotationAttributeValue>? = null + var any = false + when (attribute.name) { + "value", "allOf" -> { + values = attribute.leafValues() + } + "anyOf" -> { + any = true + values = attribute.leafValues() + } + } + + if (values == null || values.isEmpty()) { + continue + } + + // Look at macros_override.cs for the usage of these + // tags. In particular, search for def:dump_permission + + val sb = StringBuilder(100) + sb.append("Requires ") + var first = true + for (value in values) { + when { + first -> first = false + any -> sb.append(" or ") + else -> sb.append(" and ") + } + + val resolved = value.resolve() + val field = if (resolved is FieldItem) + resolved + else { + val v: Any = value.value() ?: value.toSource() + findPermissionField(codebase, v) + } + if (field == null) { + reporter.report( + Errors.MISSING_PERMISSION, item, + "Cannot find permission field for $value required by $item (may be hidden or removed)" + ) + //return + sb.append(value.toSource()) + + } else { + if (field.isHiddenOrRemoved()) { + reporter.report( + Errors.MISSING_PERMISSION, item, + "Permission $value required by $item is hidden or removed" + ) + } + sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()}}") + } + } + + appendDocumentation(sb.toString(), item, false) + } + } + + // Document value ranges + if (name == "android.support.annotation.IntRange" || name == "android.support.annotation.FloatRange") { + val from: String? = annotation.findAttribute("from")?.value?.toSource() + val to: String? = annotation.findAttribute("to")?.value?.toSource() + // TODO: inclusive/exclusive attributes on FloatRange! + if (from != null || to != null) { + val args = HashMap<String, String>() + if (from != null) args.put("from", from) + if (from != null) args.put("from", from) + if (to != null) args.put("to", to) + val doc = if (from != null && to != null) { + "Value is between $from and $to inclusive" + } else if (from != null) { + "Value is $from or greater" + } else if (to != null) { + "Value is $to or less" + } else { + null + } + appendDocumentation(doc, item, true) + } + } + + // Document expected constants + if (name == "android.support.annotation.IntDef" || name == "android.support.annotation.LongDef" + || name == "android.support.annotation.StringDef" + ) { + val values = annotation.findAttribute("value")?.leafValues() ?: return + val flag = annotation.findAttribute("flag")?.value?.toSource() == "true" + + // Look at macros_override.cs for the usage of these + // tags. In particular, search for def:dump_int_def + + val sb = StringBuilder(100) + sb.append("Value is ") + if (flag) { + sb.append("either <code>0</code> or ") + if (values.size > 1) { + sb.append("a combination of ") + } + } + + values.forEachIndexed { index, value -> + sb.append( + when (index) { + 0 -> { + "" + } + values.size - 1 -> { + if (flag) { + ", and " + } else { + ", or " + } + } + else -> { + ", " + } + } + ) + + val field = value.resolve() + if (field is FieldItem) + sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()}}") + else { + sb.append(value.toSource()) + } + } + appendDocumentation(sb.toString(), item, true) + } + + // Thread annotations are ignored here because they're handled as a group afterwards + + // TODO: Resource type annotations + + // Handle inner annotations + annotation.resolve()?.modifiers?.annotations()?.forEach { nested -> + if (depth == 20) { // Temp debugging + throw StackOverflowError( + "Unbounded recursion, processing annotation " + + "${annotation.toSource()} in $item in ${item.compilationUnit()} " + ) + } + handleAnnotation(nested, item, depth + 1) + } + } + }) + } + + /** + * Appends the given documentation to the given item. + * If it's documentation on a parameter, it is redirected to the surrounding method's + * documentation. + * + * If the [returnValue] flag is true, the documentation is added to the description text + * of the method, otherwise, it is added to the return tag. This lets for example + * a threading annotation requirement be listed as part of a method description's + * text, and a range annotation be listed as part of the return value description. + * */ + private fun appendDocumentation(doc: String?, item: Item, returnValue: Boolean) { + doc ?: return + + when (item) { + is ParameterItem -> item.containingMethod().appendDocumentation(doc, item.name()) + is MethodItem -> + // Document as part of return annotation, not member doc + item.appendDocumentation(doc, if (returnValue) "@return" else null) + else -> item.appendDocumentation(doc) + } + } + + private fun addDoc(annotation: AnnotationItem, tag: String, item: Item) { + // TODO: Cache: we shouldn't have to keep looking this up over and over + // for example for the nullable/non-nullable annotation classes that + // are used everywhere! + val cls = annotation.resolve() ?: return + + val documentation = cls.findTagDocumentation(tag) + if (documentation != null) { + assert(documentation.startsWith("@$tag"), { documentation }) + // TODO: Insert it in the right place (@return or @param) + val section = when { + documentation.startsWith("@returnDoc") -> "@return" + documentation.startsWith("@paramDoc") -> "@param" + documentation.startsWith("@memberDoc") -> null + else -> null + } + val insert = stripMetaTags(documentation.substring(tag.length + 2)) + item.appendDocumentation(insert, section) // 2: @ and space after tag + } + } + + private fun stripMetaTags(string: String): String { + // Get rid of @hide and @remove tags etc that are part of documentation snippets + // we pull in, such that we don't accidentally start applying this to the + // item that is pulling in the documentation. + if (string.contains("@hide") || string.contains("@remove")) { + return string.replace("@hide", "").replace("@remove", "") + } + return string + } + + /** Replacements to perform in documentation */ + val typos = mapOf( + "Andriod" to "Android", + "Kitkat" to "KitKat", + "LemonMeringuePie" to "Lollipop", + "LMP" to "Lollipop", + "KeyLimePie" to "KitKat", + "KLP" to "KitKat" + ) + + private fun tweakGrammar() { + codebase.accept(object : VisibleItemVisitor() { + override fun visitItem(item: Item) { + var doc = item.documentation + if (doc.isBlank()) { + return + } + + for (typo in typos.keys) { + if (doc.contains(typo)) { + val replacement = typos[typo] ?: continue + reporter.report( + Errors.TYPO, + item, + "Replaced $typo with $replacement in documentation for $item" + ) + doc = doc.replace(typo, replacement, false) + item.documentation = doc + } + } + + val firstDot = doc.indexOf(".") + if (firstDot > 0 && doc.regionMatches(firstDot - 1, "e.g. ", 0, 5, false)) { + doc = doc.substring(0, firstDot) + ".g. " + doc.substring(firstDot + 4) + item.documentation = doc + } + } + }) + } + + fun applyApiLevels(applyApiLevelsXml: File) { + val client = object : LintCliClient() { + override fun findResource(relativePath: String): File? { + if (relativePath == ApiLookup.XML_FILE_PATH) { + return applyApiLevelsXml + } + return super.findResource(relativePath) + } + + override fun getCacheDir(name: String?, create: Boolean): File? { + val dir = File(System.getProperty("java.io.tmpdir")) + if (create) { + dir.mkdirs() + } + return dir + } + } + + val apiLookup = ApiLookup.get(client) + + //codebase.accept(object : VisibleItemVisitor(visitConstructorsAsMethods = false) { + codebase.accept(object : ApiVisitor(codebase, visitConstructorsAsMethods = false) { + override fun visitMethod(method: MethodItem) { + val psiMethod = method.psi() as PsiMethod + addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method) + addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method) + } + + override fun visitClass(cls: ClassItem) { + val psiClass = cls.psi() as PsiClass + addApiLevelDocumentation(apiLookup.getClassVersion(psiClass), cls) + addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls) + } + + override fun visitField(field: FieldItem) { + val psiField = field.psi() as PsiField + addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field) + addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field) + } + + private fun addApiLevelDocumentation(level: Int, item: Item) { + if (level > 1) { + appendDocumentation("Requires API level $level", item, false) + } + } + + private fun addDeprecatedDocumentation(level: Int, item: Item) { + if (level > 1) { + // TODO: *pre*pend instead! + //val description = "This class was deprecated in API level $level. " + val description = + "<p class=\"caution\"><strong>This class was deprecated in API level 21.</strong></p>" + item.appendDocumentation(description, "@deprecated", append = false) + } + } + }) + } +} + +fun ApiLookup.getClassVersion(cls: PsiClass): Int { + val owner = cls.qualifiedName ?: return -1 + return getClassVersion(owner) +} + +fun ApiLookup.getMethodVersion(method: PsiMethod): Int { + val containingClass = method.containingClass ?: return -1 + val owner = containingClass.qualifiedName ?: return -1 + val evaluator = DefaultJavaEvaluator(null, null) + val desc = evaluator.getMethodDescription(method, false, false) + return getMethodVersion(owner, method.name, desc) +} + +fun ApiLookup.getFieldVersion(field: PsiField): Int { + val containingClass = field.containingClass ?: return -1 + val owner = containingClass.qualifiedName ?: return -1 + return getFieldVersion(owner, field.name) +} + +fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int { + val owner = cls.qualifiedName ?: return -1 + return getClassDeprecatedIn(owner) +} + +fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int { + val containingClass = method.containingClass ?: return -1 + val owner = containingClass.qualifiedName ?: return -1 + val evaluator = DefaultJavaEvaluator(null, null) + val desc = evaluator.getMethodDescription(method, false, false) + return getMethodDeprecatedIn(owner, method.name, desc) +} + +fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int { + val containingClass = field.containingClass ?: return -1 + val owner = containingClass.qualifiedName ?: return -1 + return getFieldDeprecatedIn(owner, field.name) +} \ 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 new file mode 100644 index 0000000..82c523d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -0,0 +1,686 @@ +/* + * Copyright (C) 2017 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:JvmName("Driver") + +package com.android.tools.metalava + +import com.android.SdkConstants +import com.android.SdkConstants.DOT_JAVA +import com.android.SdkConstants.DOT_KT +import com.android.tools.lint.KotlinLintAnalyzerFacade +import com.android.tools.lint.LintCoreApplicationEnvironment +import com.android.tools.lint.LintCoreProjectEnvironment +import com.android.tools.lint.annotations.Extractor +import com.android.tools.metalava.apilevels.ApiGenerator +import com.android.tools.metalava.doclava1.ApiFile +import com.android.tools.metalava.doclava1.ApiParseException +import com.android.tools.metalava.doclava1.ApiPredicate +import com.android.tools.metalava.doclava1.ElidingPredicate +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.doclava1.FilterPredicate +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.psi.PsiBasedCodebase +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.google.common.base.Stopwatch +import com.google.common.collect.Lists +import com.google.common.io.Files +import com.intellij.openapi.util.Disposer +import com.intellij.psi.PsiClassOwner +import java.io.File +import java.io.IOException +import java.io.OutputStreamWriter +import java.io.PrintWriter +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.SECONDS +import java.util.function.Predicate +import java.util.regex.Pattern +import kotlin.text.Charsets.UTF_8 + +const val PROGRAM_NAME = "metalava" +const val HELP_PROLOGUE = "$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the " + + "signature files, the SDK stub files, external annotations etc." + +@Suppress("PropertyName") // Can't mark const because trimIndent() :-( +val BANNER: String = """ + _ _ + _ __ ___ ___| |_ __ _| | __ ___ ____ _ +| '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` | +| | | | | | __/ || (_| | | (_| |\ V / (_| | +|_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_| +""".trimIndent() + +fun main(args: Array<String>) { + run(args, setExitCode = true) +} + +/** + * The metadata driver is a command line interface to extracting various metadata + * from a source tree (or existing signature files etc). Run with --help to see + * more details. + */ +fun run( + args: Array<String>, + stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)), + stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)), + setExitCode: Boolean = false +): Boolean { + + if (System.getenv("METALAVA_DUMP_ARGV") != null && + !java.lang.Boolean.getBoolean("METALAVA_TESTS_RUNNING") + ) { + stdout.println("---Running $PROGRAM_NAME----") + stdout.println("pwd=${File("").absolutePath}") + args.forEach { arg -> + stdout.println("\"$arg\",") + } + stdout.println("----------------------------") + } + + try { + val modifiedArgs = + if (args.isEmpty()) { + arrayOf("--help") + } else { + args + } + + options = Options(modifiedArgs, stdout, stderr) + compatibility = Compatibility(options.compatOutput) + processFlags() + stdout.flush() + stderr.flush() + + if (setExitCode && reporter.hasErrors()) { + System.exit(-1) + } + return true + } catch (e: Options.OptionsException) { + if (e.stderr.isNotBlank()) { + stderr.println("\n${e.stderr}") + } + if (e.stdout.isNotBlank()) { + stdout.println("\n${e.stdout}") + } + if (setExitCode) { + stdout.flush() + stderr.flush() + System.exit(e.exitCode) + } + } + stdout.flush() + stderr.flush() + return false +} + +private fun processFlags() { + val stopwatch = Stopwatch.createStarted() + + val androidApiLevelXml = options.generateApiLevelXml + val apiLevelJars = options.apiLevelJars + if (androidApiLevelXml != null && apiLevelJars != null) { + ApiGenerator.generate(apiLevelJars, androidApiLevelXml) + + if (options.apiJar == null && options.sources.isEmpty() && + options.sourcePath.isEmpty() && options.previousApi == null + ) { + // Done + return + } + } + + val codebase = + if (options.sources.size == 1 && options.sources[0].path.endsWith(SdkConstants.DOT_TXT)) { + loadFromSignatureFiles( + file = options.sources[0], kotlinStyleNulls = options.inputKotlinStyleNulls, + manifest = options.manifest, performChecks = true, supportsStagedNullability = true + ) + } else if (options.apiJar != null) { + loadFromJarFile(options.apiJar!!) + } else { + loadFromSources() + } + options.manifest?.let { codebase.manifest = it } + + if (options.verbose) { + options.stdout.println("\n$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(TimeUnit.SECONDS)} seconds") + } + + val previousApiFile = options.previousApi + if (previousApiFile != null) { + val previous = loadFromSignatureFiles( + previousApiFile, options.inputKotlinStyleNulls, + supportsStagedNullability = true + ) + codebase.description = "Source tree" + previous.description = "Previous stable API" + + // If configured, compares the new API with the previous API and reports + // any incompatibilities. + checkCompatibility(codebase, previous) + + // If configured, checks for newly added nullness information compared + // to the previous stable API and marks the newly annotated elements + // as migrated (which will cause the Kotlin compiler to treat problems + // as warnings instead of errors + + migrateNulls(codebase, previous) + } + + // Based on the input flags, generates various output files such + // as signature files and/or stubs files + generateOutputs(codebase) + + // Coverage stats? + if (options.dumpAnnotationStatistics) { + progress("\nMeasuring annotation statistics: ") + AnnotationStatistics(codebase).count() + } + if (options.annotationCoverageOf.isNotEmpty()) { + progress("\nMeasuring annotation coverage: ") + AnnotationStatistics(codebase).measureCoverageOf(options.annotationCoverageOf) + } + + Disposer.dispose(LintCoreApplicationEnvironment.get().parentDisposable) + + if (!options.quiet) { + val packageCount = codebase.size() + options.stdout.println("\n$PROGRAM_NAME finished handling $packageCount packages in $stopwatch") + options.stdout.flush() + } +} + +private fun migrateNulls(codebase: Codebase, previous: Codebase) { + if (options.migrateNulls) { + val prev = previous.supportsStagedNullability + try { + previous.supportsStagedNullability = true + previous.compareWith( + NullnessMigration(), codebase, + ApiPredicate(codebase) + ) + } finally { + previous.supportsStagedNullability = prev + } + } +} + +private fun checkCompatibility(codebase: Codebase, previous: Codebase) { + if (options.checkCompatibility) { + previous.compareWith(CompatibilityCheck(), codebase, ApiPredicate(codebase)) + } +} + +private fun loadFromSignatureFiles( + file: File, kotlinStyleNulls: Boolean, + manifest: File? = null, + performChecks: Boolean = false, + supportsStagedNullability: Boolean = false +): Codebase { + try { + val codebase = ApiFile.parseApi(File(file.path), kotlinStyleNulls, supportsStagedNullability) + codebase.manifest = manifest + codebase.description = "Codebase loaded from ${file.name}" + + if (performChecks) { + val analyzer = ApiAnalyzer(codebase) + analyzer.performChecks() + } + return codebase + } catch (ex: ApiParseException) { + val message = "Unable to parse signature file $file: ${ex.message}" + throw Options.OptionsException(message) + } +} + +private fun loadFromSources(): Codebase { + val projectEnvironment = createProjectEnvironment() + + progress("\nProcessing sources: ") + + val sources = if (options.sources.isEmpty()) { + if (!options.quiet) { + options.stdout.println("No source files specified: recursively including all sources found in the source path") + } + gatherSources(options.sourcePath) + } else { + options.sources + } + + val joined = mutableListOf<File>() + joined.addAll(options.sourcePath.map { it.absoluteFile }) + joined.addAll(options.classpath.map { it.absoluteFile }) + // Add in source roots implied by the source files + extractRoots(sources, joined) + + // Create project environment with those paths + projectEnvironment.registerPaths(joined) + val project = projectEnvironment.project + + val kotlinFiles = sources.filter { it.path.endsWith(SdkConstants.DOT_KT) } + KotlinLintAnalyzerFacade.analyze(kotlinFiles, joined, project) + + val units = Extractor.createUnitsForFiles(project, sources) + val packageDocs = gatherHiddenPackagesFromJavaDocs(options.sourcePath) + + progress("\nReading Codebase: ") + + val codebase = PsiBasedCodebase("Codebase loaded from source folders") + codebase.initialize(project, units, packageDocs) + codebase.manifest = options.manifest + + progress("\nAnalyzing API: ") + + val analyzer = ApiAnalyzer(codebase) + analyzer.mergeExternalAnnotations() + analyzer.computeApi() + analyzer.handleStripping() + + progress("\nInsert missing constructors: ") + val ignoreShown = options.showUnannotated + + // 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) + 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() + + val applyApiLevelsXml = options.applyApiLevelsXml + if (applyApiLevelsXml != null) { + progress("\nApplying API levels") + docAnalyzer.applyApiLevels(applyApiLevelsXml) + } + } + + progress("\nPerforming misc API checks: ") + analyzer.performChecks() + +// // TODO: Move the filtering earlier +// progress("\nFiltering API: ") +// return filterCodebase(codebase) + return codebase +} + +private fun filterCodebase(codebase: PsiBasedCodebase): Codebase { + val ignoreShown = options.showAnnotations.isEmpty() + + // We ignore removals when limiting the API + val apiFilter = FilterPredicate(ApiPredicate(codebase, ignoreShown = ignoreShown)) + val apiReference = ApiPredicate(codebase, ignoreShown = true) + val apiEmit = apiFilter.and(ElidingPredicate(apiReference)) + + return codebase.filter(apiEmit, apiReference) +} + +private fun loadFromJarFile(apiJar: File, manifest: File? = null): Codebase { + val projectEnvironment = createProjectEnvironment() + + progress("Processing jar file: ") + + // Create project environment with those paths + val project = projectEnvironment.project + projectEnvironment.registerPaths(listOf(apiJar)) + val codebase = PsiBasedCodebase() + codebase.initialize(project, apiJar) + if (manifest != null) { + codebase.manifest = options.manifest + } + val analyzer = ApiAnalyzer(codebase) + analyzer.mergeExternalAnnotations() + codebase.description = "Codebase loaded from ${apiJar.name}" + return codebase +} + +private fun createProjectEnvironment(): LintCoreProjectEnvironment { + ensurePsiFileCapacity() + val appEnv = LintCoreApplicationEnvironment.get() + val parentDisposable = Disposer.newDisposable() + return LintCoreProjectEnvironment.create(parentDisposable, appEnv) +} + +private fun ensurePsiFileCapacity() { + val fileSize = System.getProperty("idea.max.intellisense.filesize") + if (fileSize == null) { + // Ensure we can handle large compilation units like android.R + System.setProperty("idea.max.intellisense.filesize", "100000") + } +} + +private fun generateOutputs(codebase: Codebase) { + + options.apiFile?.let { apiFile -> + val apiFilter = FilterPredicate(ApiPredicate(codebase)) + val apiReference = ApiPredicate(codebase, ignoreShown = true) + val apiEmit = apiFilter.and(ElidingPredicate(apiReference)) + + createReportFile(codebase, apiFile, "API", { printWriter -> + val preFiltered = codebase.original != null + SignatureWriter(printWriter, apiEmit, apiReference, preFiltered) + }) + } + + options.removedApiFile?.let { apiFile -> + val unfiltered = codebase.original ?: codebase + + val removedFilter = FilterPredicate(ApiPredicate(codebase, matchRemoved = true)) + val removedReference = ApiPredicate(codebase, ignoreShown = true, ignoreRemoved = true) + val removedEmit = removedFilter.and(ElidingPredicate(removedReference)) + + createReportFile(unfiltered, apiFile, "removed API", { printWriter -> + SignatureWriter(printWriter, removedEmit, removedReference, codebase.original != null) + }) + } + + options.privateApiFile?.let { apiFile -> + val apiFilter = FilterPredicate(ApiPredicate(codebase)) + val privateEmit = apiFilter.negate() + val privateReference = Predicate<Item> { true } + + createReportFile(codebase, apiFile, "private API", { printWriter -> + SignatureWriter(printWriter, privateEmit, privateReference, codebase.original != null) + }) + } + + options.privateDexApiFile?.let { apiFile -> + val apiFilter = FilterPredicate(ApiPredicate(codebase)) + val privateEmit = apiFilter.negate() + val privateReference = Predicate<Item> { true } + + createReportFile(codebase, apiFile, "DEX API", + { printWriter -> DexApiWriter(printWriter, privateEmit, privateReference) }) + } + + options.proguard?.let { proguard -> + val apiEmit = FilterPredicate(ApiPredicate(codebase)) + val apiReference = ApiPredicate(codebase, ignoreShown = true) + createReportFile(codebase, proguard, "Proguard file", + { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }) + } + + options.stubsDir?.let { createStubFiles(it, codebase) } + // Otherwise, if we've asked to write out a file list, write out the + // input file list instead + ?: options.stubsSourceList?.let { file -> + val root = File("").absoluteFile + val sources = options.sources + val rootPath = root.path + val contents = sources.joinToString(" ") { + val path = it.path + if (path.startsWith(rootPath)) { + path.substring(rootPath.length) + } else { + path + } + } + Files.asCharSink(file, UTF_8).write(contents) + } + + options.externalAnnotations?.let { extractAnnotations(codebase, it) } + progress("\n") +} + +private fun extractAnnotations(codebase: Codebase, file: File) { + val localTimer = Stopwatch.createStarted() + val units = codebase.units + + @Suppress("UNCHECKED_CAST") + ExtractAnnotations().extractAnnotations(units.asSequence().filter { it is PsiClassOwner }.toList() as List<PsiClassOwner>) + if (options.verbose) { + options.stdout.print("\n$PROGRAM_NAME extracted annotations into $file in $localTimer") + options.stdout.flush() + } +} + +private fun createStubFiles(stubDir: File, codebase: Codebase) { + // Generating stubs from a sig-file-based codebase is problematic + assert(codebase.supportsDocumentation()) + + progress("\nGenerating stub files: ") + val localTimer = Stopwatch.createStarted() + val prevCompatibility = compatibility + if (compatibility.compat) { + //if (!options.quiet) { + // options.stderr.println("Warning: Turning off compat mode when generating stubs") + //} + compatibility = Compatibility(false) + // But preserve the setting for whether we want to erase throws signatures (to ensure the API + // stays compatible) + compatibility.useErasureInThrows = prevCompatibility.useErasureInThrows + } + + val stubWriter = + StubWriter( + codebase = codebase, stubsDir = stubDir, generateAnnotations = options.generateAnnotations, + preFiltered = codebase.original != null + ) + codebase.accept(stubWriter) + + // Optionally also write out a list of source files that were generated; used + // for example to point javadoc to the stubs output to generate documentation + options.stubsSourceList?.let { + val root = File("").absoluteFile + stubWriter.writeSourceList(it, root) + } + + /* + // Temporary hack: Also write out annotations to make stub compilation work. This is + // just temporary: the Makefiles for the platform should be updated to supply a + // bootclasspath instead. + val nullable = File(stubDir, "android/support/annotation/Nullable.java") + val nonnull = File(stubDir, "android/support/annotation/NonNull.java") + nullable.parentFile.mkdirs() + nonnull.parentFile.mkdirs() + Files.asCharSink(nullable, UTF_8).write( + "package android.support.annotation;\n" + + "import java.lang.annotation.*;\n" + + "import static java.lang.annotation.ElementType.*;\n" + + "import static java.lang.annotation.RetentionPolicy.SOURCE;\n" + + "@SuppressWarnings(\"WeakerAccess\")\n" + + "@Retention(SOURCE)\n" + + "@Target({METHOD, PARAMETER, FIELD})\n" + + "public @interface Nullable {\n" + + "}\n" + ) + Files.asCharSink(nonnull, UTF_8).write( + "package android.support.annotation;\n" + + "import java.lang.annotation.*;\n" + + "import static java.lang.annotation.ElementType.*;\n" + + "import static java.lang.annotation.RetentionPolicy.SOURCE;\n" + + "@SuppressWarnings(\"WeakerAccess\")\n" + + "@Retention(SOURCE)\n" + + "@Target({METHOD, PARAMETER, FIELD})\n" + + "public @interface NonNull {\n" + + "}\n" + ) + */ + + compatibility = prevCompatibility + + progress("\n$PROGRAM_NAME wrote stubs directory $stubDir in ${localTimer.elapsed(SECONDS)} seconds") +} + +private fun progress(message: String) { + if (options.verbose) { + options.stdout.print(message) + options.stdout.flush() + } +} + +private fun createReportFile( + codebase: Codebase, + apiFile: File, + description: String, + createVisitor: (PrintWriter) -> ApiVisitor +) { + progress("\nWriting $description file: ") + val localTimer = Stopwatch.createStarted() + try { + val writer = PrintWriter(Files.asCharSink(apiFile, Charsets.UTF_8).openBufferedStream()) + writer.use { printWriter -> + val apiWriter = createVisitor(printWriter) + codebase.accept(apiWriter) + } + } catch (e: IOException) { + reporter.report(Errors.IO_ERROR, apiFile, "Cannot open file for write.") + } + if (options.verbose) { + options.stdout.print("\n$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds") + } +} + +/** Used for verbose output to show progress bar */ +private var tick = 0 + +/** Print progress */ +fun tick() { + tick++ + if (tick % 100 == 0) { + options.stdout.print(".") + options.stdout.flush() + } +} + +private fun addSourceFiles(list: MutableList<File>, file: File) { + if (file.isDirectory) { + val files = file.listFiles() + if (files != null) { + for (child in files) { + addSourceFiles(list, child) + } + } + } else { + if (file.isFile && (file.path.endsWith(DOT_JAVA) || file.path.endsWith(DOT_KT))) { + list.add(file) + } + } +} + +fun gatherSources(sourcePath: List<File>): List<File> { + val sources = Lists.newArrayList<File>() + for (file in sourcePath) { + addSourceFiles(sources, file.absoluteFile) + } + return sources +} + +private fun addHiddenPackages( + packageToDoc: MutableMap<String, String>, + hiddenPackages: MutableSet<String>, + file: File, + pkg: String +) { + if (file.isDirectory) { + val files = file.listFiles() + if (files != null) { + for (child in files) { + val subPkg = + if (child.isDirectory) + if (pkg.isEmpty()) + child.name + else + pkg + "." + child.name + else + pkg + addHiddenPackages(packageToDoc, hiddenPackages, child, subPkg) + } + } + } else if (file.isFile && file.name == "package.html") { + val contents = Files.asCharSource(file, Charsets.UTF_8).read() + packageToDoc.put(pkg, contents) + if (contents.contains("@hide")) { + hiddenPackages.add(pkg) + } + } +} + +private fun gatherHiddenPackagesFromJavaDocs(sourcePath: List<File>): PackageDocs { + val map = HashMap<String, String>(100) + val set = HashSet<String>(100) + for (file in sourcePath) { + addHiddenPackages(map, set, file, "") + } + return PackageDocs(map, set) +} + +private fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> { + // Cache for each directory since computing root for a source file is + // expensive + val dirToRootCache = mutableMapOf<String, File>() + for (file in sources) { + val parent = file.parentFile ?: continue + val found = dirToRootCache[parent.path] + if (found != null) { + continue + } + + val root = findRoot(file) ?: continue + dirToRootCache.put(parent.path, root) + + if (!sourceRoots.contains(root)) { + sourceRoots.add(root) + } + } + + return sourceRoots +} + +/** + * If given a full path to a Java or Kotlin source file, produces the path to + * the source root if possible. + */ +private fun findRoot(file: File): File? { + val path = file.path + if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) { + val pkg = findPackage(file) ?: return null + val parent = file.parentFile ?: return null + return File(path.substring(0, parent.path.length - pkg.length)) + } + + return null +} + +/** Finds the package of the given Java/Kotlin source file, if possible */ +fun findPackage(file: File): String? { + val source = Files.asCharSource(file, Charsets.UTF_8).read() + return findPackage(source) +} + +@Suppress("PrivatePropertyName") +private val PACKAGE_PATTERN = Pattern.compile("package\\s+([\\S&&[^;]]*)") + +/** Finds the package of the given Java/Kotlin source code, if possible */ +fun findPackage(source: String): String? { + val matcher = PACKAGE_PATTERN.matcher(source) + val foundPackage = matcher.find() + return if (foundPackage) { + matcher.group(1).trim { it <= ' ' } + } else { + null + } +} + +data class PackageDocs(val packageDocs: MutableMap<String, String>, val hiddenPackages: MutableSet<String>) diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt new file mode 100644 index 0000000..f97c112 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 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.lint.annotations.Extractor +import com.intellij.psi.PsiClassOwner + +class ExtractAnnotations { + fun extractAnnotations(units: List<PsiClassOwner>) { + val rmTypeDefs = if (options.rmTypeDefs != null) listOf(options.rmTypeDefs) else emptyList() + val typedefFile = options.typedefFile + val filter = options.apiFilter + + val verbose = !options.quiet + val skipClassRetention = options.skipClassRetention + val extractor = Extractor(filter, rmTypeDefs, verbose, !skipClassRetention, true) + extractor.isListIgnored = !options.hideFiltered + extractor.extractFromProjectSource(units) + for (jar in options.mergeAnnotations) { + extractor.mergeExisting(jar) + } + + extractor.export(options.externalAnnotations, null) + + if (typedefFile != null) { + extractor.writeTypedefFile(typedefFile) + } + + if (rmTypeDefs.isNotEmpty()) { + if (typedefFile != null) { + Extractor.removeTypedefClasses(rmTypeDefs, typedefFile) + } else { + extractor.removeTypedefClasses() + } + } + } +} diff --git a/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/src/main/java/com/android/tools/metalava/NullnessMigration.kt new file mode 100644 index 0000000..60c727f --- /dev/null +++ b/src/main/java/com/android/tools/metalava/NullnessMigration.kt @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2017 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.AnnotationItem +import com.android.tools.metalava.model.Item + +/** + * Performs null migration analysis, looking at previous API signature + * files and new signature files, and replacing new @Nullable and @NonNull + * annotations with @NewlyNullable and @NewlyNonNull, and similarly + * moving @NewlyNullable and @NewlyNonNull to @RecentlyNullable and @RecentlyNonNull + * (and finally once the annotations have been there for another API level, + * finally moving them to unconditionally nullable/nonnull.) + * + * (Newly null is the initial level; user code is marked as warnings if in + * conflict with the annotation. Recently null is the next level; once an + * API has had newly-null metadata in one API level, it gets promoted to + * recently, which generates errors instead of warnings. The reason we have + * this instead of just making it unconditional is that you can still invoke + * the compiler with a flag to defeat it, so the Kotlin team suggested we do + * this. + * + * TODO: Enforce compatibility across type use annotations, e.g. + * changing parameter value from + * {@code @NonNull List<@Nullable String>} + * to + * {@code @NonNull List<@NonNull String>} + * is forbidden. + */ +class NullnessMigration : ComparisonVisitor() { + override fun compare(old: Item, new: Item) { + if (hasNullnessInformation(new)) { + if (!hasNullnessInformation(old)) { + // Nullness information change: Add migration annotation + val annotation = if (isNullable(new)) NEWLY_NULLABLE else NEWLY_NONNULL + + val migration = findNullnessAnnotation(new) ?: return + val modifiers = new.mutableModifiers() + modifiers.removeAnnotation(migration) + + // Don't map annotation names - this would turn newly non null back into non null + modifiers.addAnnotation(new.codebase.createAnnotation("@" + annotation, new, mapName = false)) + } else if (hasMigrationAnnotation(old)) { + // Already marked migration before: Now we can promote it to + // no longer migrated! + val nullAnnotation = findNullnessAnnotation(new) ?: return + val migration = findMigrationAnnotation(old)?.toSource() ?: return + val modifiers = new.mutableModifiers() + modifiers.removeAnnotation(nullAnnotation) + + if (isNewlyMigrated(old)) { + // Move from newly to recently + val source = migration.replace("Newly", "Recently") + modifiers.addAnnotation(new.codebase.createAnnotation(source, new, mapName = false)) + } else { + // Move from recently to no longer marked as migrated + val source = migration.replace("Newly", "").replace("Recently", "") + modifiers.addAnnotation(new.codebase.createAnnotation(source, new, mapName = false)) + } + } + } + } + + companion object { + fun hasNullnessInformation(item: Item): Boolean { + return isNullable(item) || isNonNull(item) + } + + fun findNullnessAnnotation(item: Item): AnnotationItem? { + return item.modifiers.annotations().firstOrNull { it.isNullnessAnnotation() } + } + + fun findMigrationAnnotation(item: Item): AnnotationItem? { + return item.modifiers.annotations().firstOrNull { + val qualifiedName = it.qualifiedName() ?: "" + isMigrationAnnotation(qualifiedName) + } + } + + fun isNullable(item: Item): Boolean { + return item.modifiers.annotations().any { it.isNullable() } + } + + fun isNonNull(item: Item): Boolean { + return item.modifiers.annotations().any { it.isNonNull() } + } + + fun hasMigrationAnnotation(item: Item): Boolean { + return item.modifiers.annotations().any { isMigrationAnnotation(it.qualifiedName() ?: "") } + } + + fun isNewlyMigrated(item: Item): Boolean { + return item.modifiers.annotations().any { isNewlyMigrated(it.qualifiedName() ?: "") } + } + + fun isRecentlyMigrated(item: Item): Boolean { + return item.modifiers.annotations().any { isRecentlyMigrated(it.qualifiedName() ?: "") } + } + + fun isNewlyMigrated(qualifiedName: String): Boolean { + return qualifiedName.endsWith(".NewlyNullable") || + qualifiedName.endsWith(".NewlyNonNull") + } + + fun isRecentlyMigrated(qualifiedName: String): Boolean { + return qualifiedName.endsWith(".RecentlyNullable") || + qualifiedName.endsWith(".RecentlyNonNull") + } + + fun isMigrationAnnotation(qualifiedName: String): Boolean { + return isNewlyMigrated(qualifiedName) || isRecentlyMigrated(qualifiedName) + } + } +} + +/** + * @TypeQualifierNickname + * @NonNull + * @kotlin.annotations.jvm.UnderMigration(status = kotlin.annotations.jvm.MigrationStatus.WARN) + * @Retention(RetentionPolicy.CLASS) + * public @interface NewlyNullable { + * } + */ +const val NEWLY_NULLABLE = "android.support.annotation.NewlyNullable" + +/** + * @TypeQualifierNickname + * @NonNull + * @kotlin.annotations.jvm.UnderMigration(status = kotlin.annotations.jvm.MigrationStatus.WARN) + * @Retention(RetentionPolicy.CLASS) + * public @interface NewlyNonNull { + * } + */ +const val NEWLY_NONNULL = "android.support.annotation.NewlyNonNull" + +/** + * @TypeQualifierNickname + * @NonNull + * @kotlin.annotations.jvm.UnderMigration(status = kotlin.annotations.jvm.MigrationStatus.STRICT) + * @Retention(RetentionPolicy.CLASS) + * public @interface NewlyNullable { + * } + */ + +const val RECENTLY_NULLABLE = "android.support.annotation.RecentlyNullable" +/** + * @TypeQualifierNickname + * @NonNull + * @kotlin.annotations.jvm.UnderMigration(status = kotlin.annotations.jvm.MigrationStatus.STRICT) + * @Retention(RetentionPolicy.CLASS) + * public @interface NewlyNonNull { + * } + */ +const val RECENTLY_NONNULL = "android.support.annotation.RecentlyNonNull" + diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt new file mode 100644 index 0000000..6fe1652 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -0,0 +1,1116 @@ +/* + * Copyright (C) 2017 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.SdkConstants +import com.android.tools.lint.annotations.ApiDatabase +import com.android.tools.lint.annotations.SdkUtils2 +import com.android.tools.lint.annotations.SdkUtils2.wrap +import com.android.tools.metalava.doclava1.Errors +import com.google.common.base.CharMatcher +import com.google.common.base.Splitter +import com.google.common.collect.Lists +import com.google.common.io.Files +import java.io.File +import java.io.IOException +import java.io.OutputStreamWriter +import java.io.PrintWriter +import java.io.StringWriter + +/** Global options for the metadata extraction tool */ +var options = Options(emptyArray()) + +private const val MAX_LINE_WIDTH = 90 + +private const val ARG_HELP = "--help" +private const val ARG_QUIET = "--quiet" +private const val ARG_VERBOSE = "--verbose" +private const val ARG_CLASS_PATH = "--classpath" +private const val ARG_SOURCE_PATH = "--source-path" +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_REMOVED_API = "--removed-api" +private const val ARG_MERGE_ANNOTATIONS = "--merge-annotations" +private const val ARG_INPUT_API_JAR = "--input-api-jar" +private const val ARG_EXACT_API = "--exact-api" +private const val ARG_STUBS = "--stubs" +private const val ARG_STUBS_SOURCE_LIST = "--write-stubs-source-list" +private const val ARG_PROGUARD = "--proguard" +private const val ARG_EXTRACT_ANNOTATIONS = "--extract-annotations" +private const val ARG_EXCLUDE_ANNOTATIONS = "--exclude-annotations" +private const val ARG_API_FILTER = "--api-filter" +private const val ARG_RM_TYPEDEFS = "--rmtypedefs" +private const val ARG_TYPEDEF_FILE = "--typedef-file" +private const val ARG_SKIP_CLASS_RETENTION = "--skip-class-retention" +private const val ARG_HIDE_FILTERED = "--hide-filtered" +private const val ARG_HIDE_PACKAGE = "--hide-package" +private const val ARG_MANIFEST = "--manifest" +private const val ARG_PREVIOUS_API = "--previous-api" +private const val ARG_MIGRATE_NULLNESS = "--migrate-nullness" +private const val ARG_CHECK_COMPATIBILITY = "--check-compatibility" +private const val ARG_INPUT_KOTLIN_NULLS = "--input-kotlin-nulls" +private const val ARG_OUTPUT_KOTLIN_NULLS = "--output-kotlin-nulls" +private const val ARG_ANNOTATION_COVERAGE_STATS = "--annotation-coverage-stats" +private const val ARG_ANNOTATION_COVERAGE_OF = "--annotation-coverage-of" +private const val ARG_WARNINGS_AS_ERRORS = "--warnings-as-errors" +private const val ARG_LINTS_AS_ERRORS = "--lints-as-errors" +private const val ARG_SHOW_ANNOTATION = "--show-annotation" +private const val ARG_SHOW_UNANNOTATED = "--show-unannotated" +private const val ARG_COLOR = "--color" +private const val ARG_NO_COLOR = "--no-color" +private const val ARG_OMIT_COMMON_PACKAGES = "--omit-common-packages" +private const val ARG_SKIP_JAVA_IN_COVERAGE_REPORT = "--skip-java-in-coverage-report" +private const val ARG_NO_BANNER = "--no-banner" +private const val ARG_ERROR = "--error" +private const val ARG_WARNING = "--warning" +private const val ARG_LINT = "--lint" +private const val ARG_HIDE = "--hide" +private const val ARG_UNHIDE_CLASSPATH_CLASSES = "--unhide-classpath-classes" +private const val ARG_ALLOW_REFERENCING_UNKNOWN_CLASSES = "--allow-referencing-unknown-classes" +private const val ARG_NO_UNKNOWN_CLASSES = "--no-unknown-classes" +private const val ARG_INCLUDE_DOC_ONLY = "--include-doconly" +private const val ARG_APPLY_API_LEVELS = "--apply-api-levels" +private const val ARG_GENERATE_API_LEVELS = "--generate-api-levels" +private const val ARG_ANDROID_JAR_PATTERN = "--android-jar-pattern" +private const val ARG_CURRENT_VERSION = "--current-version" +private const val ARG_CURRENT_CODENAME = "--current-codename" +private const val ARG_CURRENT_JAR = "--current-jar" + +class Options( + args: Array<String>, + /** Writer to direct output to */ + var stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)), + /** Writer to direct error messages to */ + var stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)) +) { + + /** Internal list backing [sources] */ + private val mutableSources: MutableList<File> = mutableListOf() + /** Internal list backing [sourcePath] */ + private val mutableSourcePath: MutableList<File> = mutableListOf() + /** Internal list backing [classpath] */ + private val mutableClassPath: MutableList<File> = mutableListOf() + /** Internal list backing [showAnnotations] */ + private val mutableShowAnnotations: MutableList<String> = mutableListOf() + /** Internal list backing [hideAnnotations] */ + private val mutableHideAnnotations: MutableList<String> = mutableListOf() + /** Internal list backing [stubImportPackages] */ + private val mutableStubImportPackages: MutableSet<String> = mutableSetOf() + /** Internal list backing [mergeAnnotations] */ + private val mutableMergeAnnotations: MutableList<File> = mutableListOf() + /** Internal list backing [annotationCoverageOf] */ + private val mutableAnnotationCoverageOf: MutableList<File> = mutableListOf() + /** Internal list backing [hidePackages] */ + private val mutableHidePackages: MutableList<String> = mutableListOf() + /** Internal list backing [skipEmitPackages] */ + private val mutableSkipEmitPackages: MutableList<String> = mutableListOf() + + /** Ignored flags we've already warned about - store here such that we don't keep reporting them */ + private val alreadyWarned: MutableSet<String> = mutableSetOf() + + /** + * Whether signature files should emit in "compat" mode, preserving the various + * quirks of the previous signature file format -- this will for example use a non-standard + * modifier ordering, it will call enums interfaces, etc. See the [Compatibility] class + * 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") + + /** Whether nullness annotations should be displayed as ?/!/empty instead of with @NonNull/@Nullable. */ + var outputKotlinStyleNulls = !compatOutput + + /** Whether we should omit common packages such as java.lang.* and kotlin.* from signature output */ + var omitCommonPackages = !compatOutput + + /** + * Whether reading signature files should assume the input is formatted as Kotlin-style nulls + * (e.g. ? means nullable, ! means unknown, empty means not null) + */ + var inputKotlinStyleNulls = false + + /** If true, treat all warnings as errors */ + var warningsAreErrors: Boolean = false + + /** If true, treat all API lint warnings as errors */ + var lintsAreErrors: Boolean = false + + /** The list of source roots */ + val sourcePath: List<File> = mutableSourcePath + + /** The list of dependency jars */ + val classpath: List<File> = mutableClassPath + + /** All source files to parse */ + var sources: List<File> = mutableSources + + /** Whether to include APIs with annotations (intended for documentation purposes) */ + var showAnnotations = mutableShowAnnotations + + /** Whether to include unannotated elements if {@link #showAnnotations} is set */ + var showUnannotated = false + + /** Packages to include (if null, include all) */ + var stubPackages: PackageFilter? = null + + /** Packages to import (if empty, include all) */ + var stubImportPackages: Set<String> = mutableStubImportPackages + + /** Packages to exclude/hide */ + var hidePackages = mutableHidePackages + + /** Packages that we should skip generating even if not hidden; typically only used by tests */ + var skipEmitPackages = mutableSkipEmitPackages + + var showAnnotationOverridesVisibility: Boolean = false + + /** Annotations to hide */ + var hideAnnotations = mutableHideAnnotations + + /** Whether to report warnings and other diagnostics along the way */ + var quiet = false + + /** Whether to report extra diagnostics along the way (note that verbose isn't the same as not quiet) */ + var verbose = false + + /** If set, a directory to write stub files to. Corresponds to the --stubs/-stubs flag. */ + var stubsDir: File? = null + + /** If set, a source file to write the stub index (list of source files) to. Can be passed to + * other tools like javac/javadoc using the special @-syntax. */ + var stubsSourceList: File? = null + + /** Proguard Keep list file to write */ + var proguard: File? = null + + /** If set, a file to write an API file to. Corresponds to the --api/-api flag. */ + var apiFile: File? = null + + /** If set, a file to write the private API file to. Corresponds to the --private-api/-privateApi flag. */ + var privateApiFile: File? = null + + /** If set, a file to write the private DEX signatures to. Corresponds to --private-dex-api. */ + var privateDexApiFile: File? = null + + /** If set, a file to write extracted annotations to. Corresponds to the --extract-annotations flag. */ + var externalAnnotations: File? = null + + /** A manifest file to read to for example look up available permissions */ + var manifest: File? = null + + /** If set, a file to write an API file to. Corresponds to the --removed-api/-removedApi flag. */ + var removedApiFile: File? = null + + /** Whether output should be colorized */ + var color = System.getenv("TERM")?.startsWith("xterm") ?: false + + /** Whether to omit Java and Kotlin runtime library packages from annotation coverage stats */ + var omitRuntimePackageStats = true + + /** Whether to include doc-only-marked items */ + var includeDocOnly = false + + /** Whether to generate annotations into the stubs */ + var generateAnnotations = true + + /** + * A signature file for the previous version of this API (for compatibility checks, nullness + * migration, etc.) + */ + var previousApi: File? = null + + /** Whether we should check API compatibility based on the previous API in [previousApi] */ + var checkCompatibility: Boolean = false + + /** Whether we should migrate nulls based on the previous API in [previousApi] */ + var migrateNulls: Boolean = false + + /** Existing external annotation files to merge in */ + var mergeAnnotations: List<File> = mutableMergeAnnotations + + /** Set of jars and class files for existing apps that we want to measure coverage of */ + var annotationCoverageOf: List<File> = mutableAnnotationCoverageOf + + /** Framework API definition to restrict included APIs to */ + var apiFilter: ApiDatabase? = null + + /** If filtering out non-APIs, supply this flag to hide listing matches */ + var hideFiltered: Boolean = false + + /** Don't extract annotations that have class retention */ + var skipClassRetention: Boolean = false + + /** Remove typedef classes found in the given folder */ + var rmTypeDefs: File? = null + + /** Framework API definition to restrict included APIs to */ + var typedefFile: File? = null + + /** An optional <b>jar</b> file to load classes from instead of from source. + * This is similar to the [classpath] attribute except we're explicitly saying + * that this is the complete set of classes and that we <b>should</b> generate + * signatures/stubs from them or use them to diff APIs with (whereas [classpath] + * is only used to resolve types.) */ + var apiJar: File? = null + + /** Whether to emit coverage statistics for annotations in the API surface */ + var dumpAnnotationStatistics = false + + /** Only used for tests: Normally we want to treat classes not found as source (e.g. supplied via + * classpath) as hidden, but for the unit tests (where we're not passing in + * a complete API surface) this makes the tests more cumbersome. + * This option lets the testing infrastructure treat these classes differently. + * To see the what this means in practice, try turning it back on for the tests + * and see what it does to the results :) + */ + var hideClasspathClasses = true + + /** Only used for tests: Whether during code filtering we allow referencing super classes + * etc that are unknown (because they're not included in the codebase) */ + var allowReferencingUnknownClasses = true + + /** Reverse of [allowReferencingUnknownClasses]: Require all classes to be known. This + * is used when compiling the main SDK itself (which includes definitions for everything, + * including java.lang.Object.) */ + var noUnknownClasses = false + + /** + * mapping from API level to android.jar files, if computing API levels + */ + var apiLevelJars: Array<File>? = null + + /** API level XML file to generate */ + var generateApiLevelXml: File? = null + + /** Reads API XML file to apply into documentation */ + var applyApiLevelsXml: File? = null + + init { + // Pre-check whether --color/--no-color is present and use that to decide how + // to emit the banner even before we emit errors + if (args.contains(ARG_NO_COLOR)) { + color = false + } else if (args.contains(ARG_COLOR) || args.contains("-android")) { + color = true + } + // empty args: only when building initial default Options (options field + // at the top of this file; replaced once the driver runs and passes in + // a real argv. Don't print a banner when initializing the default options.) + if (args.isNotEmpty() && !args.contains(ARG_QUIET) && !args.contains(ARG_NO_BANNER)) { + if (color) { + stdout.print(colorized(BANNER.trimIndent(), TerminalColor.BLUE)) + } else { + stdout.println(BANNER.trimIndent()) + } + } + stdout.println() + stdout.flush() + + val apiFilters = mutableListOf<File>() + var androidJarPatterns: MutableList<String>? = null + var currentApiLevel: Int = -1 + var currentCodeName: String? = null + var currentJar: File? = null + + var index = 0 + while (index < args.size) { + val arg = args[index] + + when (arg) { + ARG_HELP, "-h", "-?" -> { + helpAndQuit(color) + } + + ARG_QUIET -> { + quiet = true; verbose = false + } + ARG_VERBOSE -> { + verbose = true; quiet = false + } + + ARGS_COMPAT_OUTPUT -> compatOutput = true + + // For now we don't distinguish between bootclasspath and classpath + ARG_CLASS_PATH, "-classpath", "-bootclasspath" -> + mutableClassPath.addAll(stringToExistingDirsOrJars(getValue(args, ++index))) + + ARG_SOURCE_PATH, "--sources", "--sourcepath", "-sourcepath" -> { + val path = getValue(args, ++index) + if (path.endsWith(SdkConstants.DOT_JAVA)) { + throw OptionsException( + "$arg should point to a source root directory, not a source file ($path)" + ) + } + mutableSourcePath.addAll(stringToExistingDirsOrJars(path)) + } + + ARG_SOURCE_FILES -> { + val listString = getValue(args, ++index) + listString.split(",").forEach { path -> + mutableSources.addAll(stringToExistingFiles(path)) + } + } + + ARG_MERGE_ANNOTATIONS, "--merge-zips" -> mutableMergeAnnotations.addAll( + stringToExistingDirsOrFiles( + getValue(args, ++index) + ) + ) + + ARG_API, "-api" -> apiFile = stringToNewFile(getValue(args, ++index)) + + ARG_PRIVATE_API, "-privateApi" -> privateApiFile = stringToNewFile(getValue(args, ++index)) + ARG_PRIVATE_DEX_API, "-privateDexApi" -> privateDexApiFile = stringToNewFile(getValue(args, ++index)) + + ARG_REMOVED_API, "-removedApi" -> removedApiFile = stringToNewFile(getValue(args, ++index)) + + ARG_EXACT_API, "-exactApi" -> { + unimplemented(arg) // Not yet implemented (because it seems to no longer be hooked up in doclava1) + } + + ARG_MANIFEST, "-manifest" -> manifest = stringToExistingFile(getValue(args, ++index)) + + ARG_SHOW_ANNOTATION, "-showAnnotation" -> mutableShowAnnotations.add(getValue(args, ++index)) + + ARG_SHOW_UNANNOTATED, "-showUnannotated" -> showUnannotated = true + + "--showAnnotationOverridesVisibility" -> { + unimplemented(arg) + showAnnotationOverridesVisibility = true + } + + "--hideAnnotations", "-hideAnnotation" -> mutableHideAnnotations.add(getValue(args, ++index)) + + ARG_STUBS, "-stubs" -> stubsDir = stringToNewDir(getValue(args, ++index)) + ARG_STUBS_SOURCE_LIST -> stubsSourceList = stringToNewFile(getValue(args, ++index)) + + ARG_EXCLUDE_ANNOTATIONS -> generateAnnotations = false + + ARG_PROGUARD, "-proguard" -> proguard = stringToNewFile(getValue(args, ++index)) + + ARG_HIDE_PACKAGE, "-hidePackage" -> mutableHidePackages.add(getValue(args, ++index)) + + "--stub-packages", "-stubpackages" -> { + val packages = getValue(args, ++index) + val filter = stubPackages ?: run { + val newFilter = PackageFilter() + stubPackages = newFilter + newFilter + } + filter.packagePrefixes += packages.split(File.pathSeparatorChar) + } + + "--stub-import-packages", "-stubimportpackages" -> { + val packages = getValue(args, ++index) + for (pkg in packages.split(File.pathSeparatorChar)) { + mutableStubImportPackages.add(pkg) + mutableHidePackages.add(pkg) + } + } + + "--skip-emit-packages" -> { + val packages = getValue(args, ++index) + mutableSkipEmitPackages += packages.split(File.pathSeparatorChar) + } + + ARG_INPUT_API_JAR -> apiJar = stringToExistingFile(getValue(args, ++index)) + + ARG_EXTRACT_ANNOTATIONS -> externalAnnotations = stringToNewFile(getValue(args, ++index)) + + ARG_PREVIOUS_API -> previousApi = stringToExistingFile(getValue(args, ++index)) + + ARG_MIGRATE_NULLNESS -> migrateNulls = true + + ARG_CHECK_COMPATIBILITY -> { + checkCompatibility = true + + // Normally some compatibility changes are warnings but when you + // explicitly check compatibility, turn them all into errors + Errors.enforceCompatibility() + } + + ARG_ANNOTATION_COVERAGE_STATS -> dumpAnnotationStatistics = true + ARG_ANNOTATION_COVERAGE_OF -> mutableAnnotationCoverageOf.add( + stringToExistingFileOrDir( + getValue(args, ++index) + ) + ) + + ARG_ERROR, "-error" -> Errors.setErrorLevel(getValue(args, ++index), Severity.ERROR) + ARG_WARNING, "-warning" -> Errors.setErrorLevel(getValue(args, ++index), Severity.WARNING) + ARG_LINT, "-lint" -> Errors.setErrorLevel(getValue(args, ++index), Severity.LINT) + ARG_HIDE, "-hide" -> Errors.setErrorLevel(getValue(args, ++index), Severity.HIDDEN) + + ARG_WARNINGS_AS_ERRORS, "-werror" -> warningsAreErrors = true + ARG_LINTS_AS_ERRORS, "-lerror" -> lintsAreErrors = true + + ARG_COLOR -> color = true + ARG_NO_COLOR -> color = false + + ARG_OMIT_COMMON_PACKAGES, ARG_OMIT_COMMON_PACKAGES + "=yes" -> omitCommonPackages = true + ARG_OMIT_COMMON_PACKAGES + "=no" -> omitCommonPackages = false + + ARG_SKIP_JAVA_IN_COVERAGE_REPORT -> omitRuntimePackageStats = true + + ARG_UNHIDE_CLASSPATH_CLASSES -> hideClasspathClasses = false + ARG_ALLOW_REFERENCING_UNKNOWN_CLASSES -> allowReferencingUnknownClasses = true + ARG_NO_UNKNOWN_CLASSES -> noUnknownClasses = true + + ARG_INCLUDE_DOC_ONLY -> includeDocOnly = true + + // Annotation extraction flags + ARG_API_FILTER -> apiFilters.add(stringToExistingFile(getValue(args, ++index))) + ARG_RM_TYPEDEFS -> rmTypeDefs = stringToExistingDir(getValue(args, ++index)) + ARG_TYPEDEF_FILE -> typedefFile = stringToNewFile(getValue(args, ++index)) + ARG_HIDE_FILTERED -> hideFiltered = true + ARG_SKIP_CLASS_RETENTION -> skipClassRetention = true + + // Extracting API levels + ARG_ANDROID_JAR_PATTERN -> { + val list = androidJarPatterns ?: run { + val list = arrayListOf<String>() + androidJarPatterns = list + list + } + list.add(getValue(args, ++index)) + } + ARG_CURRENT_VERSION -> { + currentApiLevel = Integer.parseInt(getValue(args, ++index)) + if (currentApiLevel <= 26) { + throw OptionsException("Suspicious currentApi=$currentApiLevel, expected at least 27") + } + } + ARG_CURRENT_CODENAME -> { + currentCodeName = getValue(args, ++index) + } + ARG_CURRENT_JAR -> { + currentJar = stringToExistingFile(getValue(args, ++index)) + } + ARG_GENERATE_API_LEVELS -> { + generateApiLevelXml = stringToNewFile(getValue(args, ++index)) + } + ARG_APPLY_API_LEVELS -> { + applyApiLevelsXml = stringToExistingFile(getValue(args, ++index)) + } + + // Unimplemented doclava1 flags (no arguments) + "-quiet" -> { + unimplemented(arg) + } + + "-android" -> { // partially implemented: Pick up the color hint, but there may be other implications + color = true + unimplemented(arg) + } + + "-stubsourceonly" -> { + /* noop */ + } + + // Unimplemented doclava1 flags (1 argument) + "-d" -> { + unimplemented(arg) + index++ + } + + "-encoding" -> { + val value = getValue(args, ++index) + if (value.toUpperCase() != "UTF-8") { + throw OptionsException("$value: Only UTF-8 encoding is supported") + } + } + + "-source" -> { + val value = getValue(args, ++index) + if (value != "1.8") { + throw OptionsException("$value: Only source 1.8 is supported") + } + } + + // Unimplemented doclava1 flags (2 arguments) + "-since" -> { + unimplemented(arg) + index += 2 + } + + // doclava1 doc-related flags: only supported here to make this command a drop-in + // replacement + "-referenceonly", + "-nodocs" -> { + javadoc(arg) + } + + // doclava1 flags with 1 argument + "-doclet", + "-docletpath", + "-templatedir", + "-htmldir", + "-knowntags", + "-resourcesdir", + "-resourcesoutdir", + "-overview" -> { + javadoc(arg) + index++ + } + + // doclava1 flags with two arguments + "-federate", + "-federationapi" -> { + javadoc(arg) + index += 2 + } + + // doclava1 flag with variable number of arguments; skip everything until next arg + "-hdf" -> { + javadoc(arg) + index++ + while (index < args.size) { + if (args[index].startsWith("-")) { + break + } + index++ + } + index-- + } + + else -> { + if (arg.startsWith("-J-") || arg.startsWith("-XD")) { + // -J: mechanism to pass extra flags to javadoc, e.g. + // -J-XX:-OmitStackTraceInFastThrow + // -XD: mechanism to set properties, e.g. + // -XDignore.symbol.file + javadoc(arg) + } else if (arg.startsWith(ARG_OUTPUT_KOTLIN_NULLS)) { + outputKotlinStyleNulls = if (arg == ARG_OUTPUT_KOTLIN_NULLS) { + true + } else { + yesNo(arg.substring(ARG_OUTPUT_KOTLIN_NULLS.length + 1)) + } + } else if (arg.startsWith(ARG_INPUT_KOTLIN_NULLS)) { + inputKotlinStyleNulls = if (arg == ARG_INPUT_KOTLIN_NULLS) { + true + } else { + yesNo(arg.substring(ARG_INPUT_KOTLIN_NULLS.length + 1)) + } + } else if (arg.startsWith(ARG_OMIT_COMMON_PACKAGES)) { + omitCommonPackages = if (arg == ARG_OMIT_COMMON_PACKAGES) { + true + } else { + yesNo(arg.substring(ARG_OMIT_COMMON_PACKAGES.length + 1)) + } + } else if (arg.startsWith(ARGS_COMPAT_OUTPUT)) { + compatOutput = if (arg == ARGS_COMPAT_OUTPUT) + true + else + yesNo(arg.substring(ARGS_COMPAT_OUTPUT.length + 1)) + } else if (arg.startsWith("-")) { + val usage = getUsage(includeHeader = false, colorize = color) + throw OptionsException(stderr = "Invalid argument $arg\n\n$usage") + } else { + // All args that don't start with "-" are taken to be filenames + mutableSources.addAll(stringToExistingFiles(arg)) + } + } + } + + ++index + } + + if (!apiFilters.isEmpty()) { + apiFilter = try { + val lines = Lists.newArrayList<String>() + for (file in apiFilters) { + lines.addAll(Files.readLines(file, com.google.common.base.Charsets.UTF_8)) + } + ApiDatabase(lines) + } catch (e: IOException) { + throw OptionsException("Could not open API database $apiFilters: ${e.localizedMessage}") + } + } + + if (generateApiLevelXml != null) { + if (currentJar != null && currentApiLevel == -1 || currentJar == null && currentApiLevel != -1) { + throw OptionsException("You must specify both --current-jar and --current-version (or neither one)") + } + if (androidJarPatterns == null) { + androidJarPatterns = mutableListOf( + "prebuilts/tools/common/api-versions/android-%/android.jar", + "prebuilts/sdk/%/android.jar" + ) + } + apiLevelJars = findAndroidJars(androidJarPatterns!!, currentApiLevel, currentCodeName, currentJar) + } + + // If the caller has not explicitly requested that unannotated classes and + // members should be shown in the output then only show them if no annotations were provided. + if (!showUnannotated && showAnnotations.isEmpty()) { + showUnannotated = true + } + + if (noUnknownClasses) { + allowReferencingUnknownClasses = false + } + + checkFlagConsistency() + } + + private fun findAndroidJars( + androidJarPatterns: List<String>, currentApiLevel: Int, + currentCodeName: String?, currentJar: File? + ): Array<File> { + + @Suppress("NAME_SHADOWING") + val currentApiLevel = if (currentCodeName != null && "REL" != currentCodeName) { + currentApiLevel + 1 + } else { + currentApiLevel + } + + val apiLevelFiles = mutableListOf<File>() + apiLevelFiles.add(File("")) // api level 0: dummy + val minApi = 1 + + // Get all the android.jar. They are in platforms-# + var apiLevel = minApi - 1 + while (true) { + apiLevel++ + try { + var jar: File? = null + if (apiLevel == currentApiLevel) { + jar = currentJar + } + if (jar == null) { + jar = getAndroidJarFile(apiLevel, androidJarPatterns) + } + if (jar == null || !jar.isFile) { + if (verbose) { + stdout.println("Last API level found: ${apiLevel - 1}") + } + break + } + if (verbose) { + stdout.println("Found API $apiLevel at ${jar.path}") + } + apiLevelFiles.add(jar) + } catch (e: IOException) { + e.printStackTrace() + } + } + + return apiLevelFiles.toTypedArray() + } + + private fun getAndroidJarFile(apiLevel: Int, patterns: List<String>): File? { + return patterns + .map { File(it.replace("%", Integer.toString(apiLevel))) } + .firstOrNull { it.isFile } + } + + private fun yesNo(answer: String): Boolean { + return when (answer) { + "yes", "true", "enabled", "on" -> true + "no", "false", "disabled", "off" -> false + else -> throw OptionsException(stderr = "Unexpected $answer; expected yes or no") + } + } + + /** Makes sure that the flag combinations make sense */ + private fun checkFlagConsistency() { + if (checkCompatibility && previousApi == null) { + throw OptionsException(stderr = "$ARG_CHECK_COMPATIBILITY requires $ARG_PREVIOUS_API") + } + + if (migrateNulls && previousApi == null) { + throw OptionsException(stderr = "$ARG_MIGRATE_NULLNESS requires $ARG_PREVIOUS_API") + } + + if (apiJar != null && sources.isNotEmpty()) { + throw OptionsException(stderr = "Specify either $ARG_SOURCE_FILES or $ARG_INPUT_API_JAR, not both") + } + + if (compatOutput && outputKotlinStyleNulls) { + throw OptionsException( + stderr = "$ARG_OUTPUT_KOTLIN_NULLS should not be combined with " + + "$ARGS_COMPAT_OUTPUT=yes" + ) + } + +// if (stubsSourceList != null && stubsDir == null) { +// throw OptionsException(stderr = "$ARG_STUBS_SOURCE_LIST should only be used when $ARG_STUBS is set") +// } + } + + private fun javadoc(arg: String) { + if (!alreadyWarned.add(arg)) { + return + } + if (!options.quiet) { + reporter.report( + Severity.WARNING, null as String?, "Ignoring javadoc-related doclava1 flag $arg", + color = color + ) + } + } + + private fun unimplemented(arg: String) { + if (!alreadyWarned.add(arg)) { + return + } + if (!options.quiet) { + val message = "Ignoring unimplemented doclava1 flag $arg" + + when (arg) { + "-encoding" -> " (UTF-8 assumed)" + "-source" -> " (1.8 assumed)" + else -> "" + } + reporter.report(Severity.WARNING, null as String?, message, color = color) + + } + } + + private fun helpAndQuit(colorize: Boolean = color) { + throw OptionsException(stdout = getUsage(colorize = colorize)) + } + + private fun getValue(args: Array<String>, index: Int): String { + if (index >= args.size) { + throw OptionsException("Missing argument for ${args[index - 1]}") + } + return args[index] + } + + private fun stringToExistingDir(value: String): File { + val file = File(value) + if (!file.isDirectory) { + throw OptionsException("$file is not a directory") + } + return file + } + + private fun stringToExistingDirs(value: String): List<File> { + val files = mutableListOf<File>() + for (path in value.split(File.pathSeparatorChar)) { + val file = File(path) + if (!file.isDirectory) { + throw OptionsException("$file is not a directory") + } + files.add(file) + } + return files + } + + private fun stringToExistingDirsOrJars(value: String): List<File> { + val files = mutableListOf<File>() + for (path in value.split(File.pathSeparatorChar)) { + val file = File(path) + if (!file.isDirectory && !(file.path.endsWith(SdkConstants.DOT_JAR) && file.isFile)) { + throw OptionsException("$file is not a jar or directory") + } + files.add(file) + } + return files + } + + private fun stringToExistingDirsOrFiles(value: String): List<File> { + val files = mutableListOf<File>() + for (path in value.split(File.pathSeparatorChar)) { + val file = File(path) + if (!file.exists()) { + throw OptionsException("$file does not exist") + } + files.add(file) + } + return files + } + + private fun stringToExistingFile(value: String): File { + val file = File(value) + if (!file.isFile) { + throw OptionsException("$file is not a file") + } + return file + } + + private fun stringToExistingFileOrDir(value: String): File { + val file = File(value) + if (!file.exists()) { + throw OptionsException("$file is not a file or directory") + } + return file + } + + private fun stringToExistingFiles(value: String): List<File> { + val files = mutableListOf<File>() + value.split(File.pathSeparatorChar) + .map { File(it) } + .forEach { file -> + if (file.path.startsWith("@")) { + // File list; files to be read are stored inside. SHOULD have been one per line + // but sadly often uses spaces for separation too (so we split by whitespace, + // which means you can't point to files in paths with spaces) + val listFile = File(file.path.substring(1)) + if (!listFile.isFile) { + throw OptionsException("$listFile is not a file") + } + val contents = Files.asCharSource(listFile, Charsets.UTF_8).read() + val pathList = Splitter.on(CharMatcher.whitespace()).trimResults().omitEmptyStrings().split( + contents + ) + pathList.asSequence().map { File(it) }.forEach { + if (!it.isFile) { + throw OptionsException("$it is not a file") + } + files.add(it) + } + } else { + if (!file.isFile) { + throw OptionsException("$file is not a file") + } + files.add(file) + } + } + return files + } + + private fun stringToNewFile(value: String): File { + val output = File(value) + + if (output.exists()) { + if (output.isDirectory) { + throw OptionsException("$output is a directory") + } + val deleted = output.delete() + if (!deleted) { + throw OptionsException("Could not delete previous version of $output") + } + } else if (output.parentFile != null && !output.parentFile.exists()) { + val ok = output.parentFile.mkdirs() + if (!ok) { + throw OptionsException("Could not create ${output.parentFile}") + } + } + + return output + } + + private fun stringToNewDir(value: String): File { + val output = File(value) + + if (output.exists()) { + if (output.isDirectory) { + output.deleteRecursively() + } + } else if (output.parentFile != null && !output.parentFile.exists()) { + val ok = output.parentFile.mkdirs() + if (!ok) { + throw OptionsException("Could not create ${output.parentFile}") + } + } + + return output + } + + private fun getUsage(includeHeader: Boolean = true, colorize: Boolean = color): String { + val usage = StringWriter() + val printWriter = PrintWriter(usage) + usage(printWriter, includeHeader, colorize) + return usage.toString() + } + + private fun usage(out: PrintWriter, includeHeader: Boolean = true, colorize: Boolean = color) { + if (includeHeader) { + out.println(wrap(HELP_PROLOGUE, MAX_LINE_WIDTH, "")) + } + + if (colorize) { + out.println("Usage: ${colorized(PROGRAM_NAME, TerminalColor.BLUE)} <flags>") + } else { + out.println("Usage: $PROGRAM_NAME <flags>") + } + + val args = arrayOf( + "", "\nGeneral:", + ARG_HELP, "This message.", + ARG_QUIET, "Only include vital output", + ARG_VERBOSE, "Include extra diagnostic output", + ARG_COLOR, "Attempt to colorize the output (defaults to true if \$TERM is xterm)", + ARG_NO_COLOR, "Do not attempt to colorize the output", + + "", "\nAPI sources:", + ARG_SOURCE_FILES + " <files>", "A comma separated list of source files to be parsed. Can also be " + + "@ followed by a path to a text file containing paths to the full set of files to parse.", + + ARG_SOURCE_PATH + " <paths>", "One or more directories (separated by `${File.pathSeparator}`) " + + "containing source files (within a package hierarchy)", + + ARG_CLASS_PATH + " <paths>", "One or more directories or jars (separated by " + + "`${File.pathSeparator}`) containing classes that should be on the classpath when parsing the " + + "source files", + + ARG_MERGE_ANNOTATIONS + " <file>", "An external annotations file (using IntelliJ's external " + + "annotations database format) to merge and overlay the sources", + + ARG_INPUT_API_JAR + " <file>", "A .jar file to read APIs from directly", + + ARG_MANIFEST + " <file>", "A manifest file, used to for check permissions to cross check APIs", + + ARG_HIDE_PACKAGE + " <package>", "Remove the given packages from the API even if they have not been " + + "marked with @hide", + + 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", + + "", "\nExtracting Signature Files:", + // TODO: Document --show-annotation! + ARG_API + " <file>", "Generate a signature descriptor file", + ARG_PRIVATE_API + " <file>", "Generate a signature descriptor file listing the exact private APIs", + ARG_PRIVATE_DEX_API + " <file>", "Generate a DEX signature descriptor file listing the exact private APIs", + ARG_REMOVED_API + " <file>", "Generate a signature descriptor file for APIs that have been removed", + ARG_OUTPUT_KOTLIN_NULLS + "[=yes|no]", "Controls whether nullness annotations should be formatted as " + + "in Kotlin (with \"?\" for nullable types, \"\" for non nullable types, and \"!\" for unknown. " + + "The default is yes.", + ARGS_COMPAT_OUTPUT + "=[yes|no]", "Controls whether to keep signature files compatible with the " + + "historical format (with its various quirks) or to generate the new format (which will also include " + + "annotations that are part of the API, etc.)", + ARG_OMIT_COMMON_PACKAGES + "[=yes|no]", "Skip common package prefixes like java.lang.* and " + + "kotlin.* in signature files, along with packages for well known annotations like @Nullable and " + + "@NonNull.", + + ARG_PROGUARD + " <file>", "Write a ProGuard keep file for the API", + + "", "\nGenerating Stubs:", + ARG_STUBS + " <dir>", "Generate stub source files for the API", + ARG_EXCLUDE_ANNOTATIONS, "Exclude annotations such as @Nullable from the stub files", + ARG_STUBS_SOURCE_LIST + " <file>", "Write the list of generated stub files into the given source " + + "list file", + + "", "\nDiffs and Checks:", + ARG_PREVIOUS_API + " <signature file>", "A signature file for the previous version of this " + + "API to apply diffs with", + ARG_INPUT_KOTLIN_NULLS + "[=yes|no]", "Whether the signature file being read should be " + + "interpreted as having encoded its types using Kotlin style types: a suffix of \"?\" for nullable " + + "types, no suffix for non nullable types, and \"!\" for unknown. The default is no.", + ARG_CHECK_COMPATIBILITY, "Check compatibility with the previous API", + ARG_MIGRATE_NULLNESS, "Compare nullness information with the previous API and mark newly " + + "annotated APIs as under migration.", + ARG_WARNINGS_AS_ERRORS, "Promote all warnings to errors", + ARG_LINTS_AS_ERRORS, "Promote all API lint warnings to errors", + ARG_ERROR + " <id>", "Report issues of the given id as errors", + ARG_WARNING + " <id>", "Report issues of the given id as warnings", + ARG_LINT + " <id>", "Report issues of the given id as having lint-severity", + ARG_HIDE + " <id>", "Hide/skip issues of the given id", + + "", "\nStatistics:", + ARG_ANNOTATION_COVERAGE_STATS, "Whether $PROGRAM_NAME should emit coverage statistics for " + + "annotations, listing the percentage of the API that has been annotated with nullness information.", + + ARG_ANNOTATION_COVERAGE_OF + " <paths>", "One or more jars (separated by `${File.pathSeparator}`) " + + "containing existing apps that we want to measure annotation coverage statistics for. The set of " + + "API usages in those apps are counted up and the most frequently used APIs that are missing " + + "annotation metadata are listed in descending order.", + + ARG_SKIP_JAVA_IN_COVERAGE_REPORT, "In the coverage annotation report, skip java.** and kotlin.** to " + + "narrow the focus down to the Android framework APIs.", + + "", "\nExtracting Annotations:", + ARG_EXTRACT_ANNOTATIONS + " <zipfile>", "Extracts annotations from the source files and writes them " + + "into the given zip file", + + ARG_API_FILTER + " <file>", "Applies the given signature file as a filter (which means no classes," + + "methods or fields not found in the filter will be included.)", + ARG_HIDE_FILTERED, "Omit listing APIs that were skipped because of the $ARG_API_FILTER", + + ARG_SKIP_CLASS_RETENTION, "Do not extract annotations that have class file retention", + ARG_RM_TYPEDEFS, "Delete all the typedef .class files", + ARG_TYPEDEF_FILE + " <file>", "Writes an typedef annotation class names into the given file", + + "", "\nInjecting API Levels:", + ARG_APPLY_API_LEVELS + " <api-versions.xml>", "Reads an XML file containing API level descriptions " + + "and merges the information into the documentation", + + "", "\nExtracting API Levels:", + ARG_GENERATE_API_LEVELS + " <xmlfile>", + "Reads android.jar SDK files and generates an XML file recording " + + "the API level for each class, method and field", + ARG_ANDROID_JAR_PATTERN + " <pattern>", "Patterns to use to locate Android JAR files. The default " + + "is \$ANDROID_HOME/platforms/android-%/android.jar.", + ARG_CURRENT_VERSION, "Sets the current API level of the current source code", + ARG_CURRENT_CODENAME, "Sets the code name for the current source code", + ARG_CURRENT_JAR, "Points to the current API jar, if any" + ) + + var argWidth = 0 + var i = 0 + while (i < args.size) { + val arg = args[i] + argWidth = Math.max(argWidth, arg.length) + i += 2 + } + argWidth += 2 + val sb = StringBuilder(20) + for (indent in 0 until argWidth) { + sb.append(' ') + } + val indent = sb.toString() + val formatString = "%1$-" + argWidth + "s%2\$s" + + i = 0 + while (i < args.size) { + val arg = args[i] + val description = args[i + 1] + if (arg.isEmpty()) { + if (colorize) { + out.println(colorized(description, TerminalColor.YELLOW)) + } else { + out.println(description) + } + } else { + if (colorize) { + val colorArg = bold(arg) + val invisibleChars = colorArg.length - arg.length + // +invisibleChars: the extra chars in the above are counted but don't contribute to width + // so allow more space + val colorFormatString = "%1$-" + (argWidth + invisibleChars) + "s%2\$s" + + out.print( + SdkUtils2.wrap( + String.format(colorFormatString, colorArg, description), + MAX_LINE_WIDTH + invisibleChars, MAX_LINE_WIDTH, indent + ) + ) + } else { + out.print( + SdkUtils2.wrap( + String.format(formatString, arg, description), + MAX_LINE_WIDTH, indent + ) + ) + } + } + i += 2 + } + } + + class OptionsException( + val stderr: String = "", + val stdout: String = "", + val exitCode: Int = if (stderr.isBlank()) 0 else -1 + ) : RuntimeException(stdout + stderr) +} diff --git a/src/main/java/com/android/tools/metalava/PackageFilter.kt b/src/main/java/com/android/tools/metalava/PackageFilter.kt new file mode 100644 index 0000000..089265d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/PackageFilter.kt @@ -0,0 +1,26 @@ +package com.android.tools.metalava + +import com.android.tools.metalava.model.PackageItem + +class PackageFilter( + val packagePrefixes: MutableList<String> = mutableListOf() +) { + + fun matches(qualifiedName: String): Boolean { + for (prefix in packagePrefixes) { + if (qualifiedName.startsWith(prefix)) { + if (qualifiedName == prefix) { + return true + } else if (qualifiedName[prefix.length] == '.') { + return true + } + } + } + + return false + } + + fun matches(packageItem: PackageItem): Boolean { + return matches(packageItem.qualifiedName()) + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/ProguardWriter.kt b/src/main/java/com/android/tools/metalava/ProguardWriter.kt new file mode 100644 index 0000000..f43a9ed --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ProguardWriter.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 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.ConstructorItem +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import java.io.PrintWriter +import java.util.function.Predicate + +class ProguardWriter( + private val writer: PrintWriter, + filterEmit: Predicate<Item>, + filterReference: Predicate<Item> +) : ApiVisitor( + visitConstructorsAsMethods = false, + nestInnerClasses = false, + inlineInheritedFields = true, + filterEmit = filterEmit, + filterReference = filterReference +) { + + override fun visitClass(cls: ClassItem) { + writer.print("-keep class ") + writer.print(cls.qualifiedNameWithDollarInnerClasses()) + writer.print(" {\n") + } + + override fun afterVisitClass(cls: ClassItem) { + writer.print("}\n") + } + + override fun visitConstructor(constructor: ConstructorItem) { + writer.print(" ") + writer.print("<init>") + + writeParametersKeepList(constructor.parameters()) + writer.print(";\n") + } + + override fun visitMethod(method: MethodItem) { + writer.print(" ") + val modifiers = method.modifiers + when { + modifiers.isPublic() -> writer.write("public ") + modifiers.isProtected() -> writer.write("protected ") + modifiers.isPrivate() -> writer.write("private ") + } + + if (modifiers.isStatic()) { + writer.print("static ") + } + if (modifiers.isAbstract()) { + writer.print("abstract ") + } + if (modifiers.isSynchronized()) { + writer.print("synchronized ") + } + + writer.print(getCleanTypeName(method.returnType())) + writer.print(" ") + writer.print(method.name()) + + writeParametersKeepList(method.parameters()) + + writer.print(";\n") + } + + private fun writeParametersKeepList(params: List<ParameterItem>) { + writer.print("(") + + for (pi in params) { + if (pi !== params[0]) { + writer.print(", ") + } + writer.print(getCleanTypeName(pi.type())) + } + + writer.print(")") + } + + override fun visitField(field: FieldItem) { + writer.print(" ") + + val modifiers = field.modifiers + when { + modifiers.isPublic() -> writer.write("public ") + modifiers.isProtected() -> writer.write("protected ") + modifiers.isPrivate() -> writer.write("private ") + } + + if (modifiers.isStatic()) { + writer.print("static ") + } + if (modifiers.isTransient()) { + writer.print("transient ") + } + if (modifiers.isVolatile()) { + writer.print("volatile ") + } + + writer.print(getCleanTypeName(field.type())) + + writer.print(" ") + writer.print(field.name()) + + writer.print(";\n") + } + + private fun getCleanTypeName(t: TypeItem?): String { + t ?: return "" + val cls = t.asClass() ?: return t.toSimpleType() + return cls.qualifiedNameWithDollarInnerClasses() + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/Reporter.kt b/src/main/java/com/android/tools/metalava/Reporter.kt new file mode 100644 index 0000000..189de9e --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Reporter.kt @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2017 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.SdkConstants.ATTR_VALUE +import com.android.tools.metalava.Severity.ERROR +import com.android.tools.metalava.Severity.HIDDEN +import com.android.tools.metalava.Severity.INHERIT +import com.android.tools.metalava.Severity.LINT +import com.android.tools.metalava.Severity.WARNING +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.model.AnnotationArrayAttributeValue +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.psi.PsiItem +import com.android.tools.metalava.model.text.TextItem +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiElement +import com.intellij.psi.impl.light.LightElement +import java.io.File + +var reporter = Reporter() + +enum class Severity(private val displayName: String) { + INHERIT("inherit"), + + HIDDEN("hidden"), + + /** + * Lint level means that we encountered inconsistent or broken documentation. + * These should be resolved, but don't impact API compatibility. + */ + LINT("lint"), + + /** + * Warning level means that we encountered some incompatible or inconsistent + * API change. These must be resolved to preserve API compatibility. + */ + WARNING("warning"), + + /** + * Error level means that we encountered severe trouble and were unable to + * output the requested documentation. + */ + ERROR("error"); + + override fun toString(): String = displayName +} + +open class Reporter(private val rootFolder: File? = null) { + var hasErrors = false + + fun error(item: Item?, message: String, id: Errors.Error? = null) { + error(item?.psi(), message, id) + } + + fun warning(item: Item?, message: String, id: Errors.Error? = null) { + warning(item?.psi(), message, id) + } + + fun error(element: PsiElement?, message: String, id: Errors.Error? = null) { + // Using lowercase since that's the convention doclava1 is using + report(ERROR, element, message, id) + } + + fun warning(element: PsiElement?, message: String, id: Errors.Error? = null) { + report(WARNING, element, message, id) + } + + fun report(id: Errors.Error, element: PsiElement?, message: String) { + report(id.level, element, message, id) + } + + fun report(id: Errors.Error, file: File?, message: String) { + report(id.level, file?.path, message, id) + } + + fun report(id: Errors.Error, item: Item?, message: String) { + if (isSuppressed(id, item)) { + return + } + + when (item) { + is PsiItem -> report(id.level, item.psi(), message, id) + is TextItem -> report(id.level, (item as? TextItem)?.position.toString(), message, id) + else -> report(id.level, "<unknown location>", message, id) + } + } + + private fun isSuppressed(id: Errors.Error, item: Item?): Boolean { + item ?: return false + + if (id.level == LINT || id.level == WARNING) { + val id1 = "Doclava${id.code}" + val id2 = id.name + val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint") + if (annotation != null) { + val attribute = annotation.findAttribute(ATTR_VALUE) + if (attribute != null) { + val value = attribute.value + if (value is AnnotationArrayAttributeValue) { + // Example: @SuppressLint({"DocLava1", "DocLava2"}) + for (innerValue in value.values) { + val string = innerValue.value() + if (id1 == string || id2 != null && id2 == string) { + return true + } + } + } else { + // Example: @SuppressLint("DocLava1") + val string = value.value() + if (id1 == string || id2 != null && id2 == string) { + return true + } + } + } + } + } + + return false + } + + private fun getTextRange(element: PsiElement): TextRange? { + var range: TextRange? = null + + if (element is PsiCompiledElement) { + if (element is LightElement) { + range = (element as PsiElement).textRange + } + if (range == null || TextRange.EMPTY_RANGE == range) { + return null + } + } else { + range = element.textRange + } + + return range + } + + private fun elementToLocation(element: PsiElement?): String? { + element ?: return null + val psiFile = element.containingFile ?: return null + val virtualFile = psiFile.virtualFile ?: return null + val file = VfsUtilCore.virtualToIoFile(virtualFile) + + val path = + if (rootFolder != null) { + val root: VirtualFile? = StandardFileSystems.local().findFileByPath(rootFolder.path) + if (root != null) VfsUtilCore.getRelativePath(virtualFile, root) else file.path + } else { + file.path + } + + val range = getTextRange(element) + return if (range == null) { + // No source offsets, just use filename + path + } else { + val lineNumber = getLineNumber(psiFile.text, range.startOffset) + path + ":" + lineNumber + } + } + + private fun getLineNumber(text: String, offset: Int): Int { + var line = 0 + var curr = offset + val length = text.length + while (curr < length) { + if (text[curr++] == '\n') { + line++ + } + } + return line + } + + open fun report(severity: Severity, element: PsiElement?, message: String, id: Errors.Error? = null) { + if (severity == HIDDEN) { + return + } + + report(severity, elementToLocation(element), message, id) + } + + open fun report( + severity: Severity, location: String?, message: String, id: Errors.Error? = null, + color: Boolean = options.color + ) { + if (severity == HIDDEN) { + return + } + + val effectiveSeverity = + if (severity == LINT && options.lintsAreErrors) + ERROR + else if (severity == WARNING && options.warningsAreErrors) { + ERROR + } else { + severity + } + + if (severity == ERROR) { + hasErrors = true + } + + val sb = StringBuilder(100) + + if (color) { + sb.append(terminalAttributes(bold = true)) + location?.let { sb.append(it).append(": ") } + when (effectiveSeverity) { + LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") + WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") + ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") + INHERIT, HIDDEN -> { + } + } + sb.append(resetTerminal()) + sb.append(message) + id?.let { sb.append(" [").append(if (it.name != null) it.name else it.code).append("]") } + } else { + location?.let { sb.append(it).append(": ") } + if (compatibility.oldErrorOutputFormat) { + // according to doclava1 there are some people or tools parsing old format + when (effectiveSeverity) { + LINT -> sb.append("lint ") + WARNING -> sb.append("warning ") + ERROR -> sb.append("error ") + INHERIT, HIDDEN -> { + } + } + id?.let { sb.append(if (it.name != null) it.name else it.code).append(": ") } + sb.append(message) + } else { + when (effectiveSeverity) { + LINT -> sb.append("lint: ") + WARNING -> sb.append("warning: ") + ERROR -> sb.append("error: ") + INHERIT, HIDDEN -> { + } + } + sb.append(message) + id?.let { + sb.append(" [") + if (it.name != null) { + sb.append(it.name).append(":") + } + sb.append(it.code) + sb.append("]") + } + } + } + print(sb.toString()) + } + + open fun print(message: String) { + options.stdout.println() + options.stdout.print(message.trim()) + options.stdout.flush() + } + + fun hasErrors(): Boolean = hasErrors +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/src/main/java/com/android/tools/metalava/SignatureWriter.kt new file mode 100644 index 0000000..49f362c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/SignatureWriter.kt @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2017 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.ConstructorItem +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import java.io.PrintWriter +import java.util.function.Predicate + +class SignatureWriter( + private val writer: PrintWriter, + filterEmit: Predicate<Item>, + filterReference: Predicate<Item>, + private val preFiltered: Boolean +) : ApiVisitor( + visitConstructorsAsMethods = false, + nestInnerClasses = false, + inlineInheritedFields = true, + methodComparator = MethodItem.comparator, + fieldComparator = FieldItem.comparator, + filterEmit = filterEmit, + filterReference = filterReference +) { + + override fun visitPackage(pkg: PackageItem) { + writer.print("package ${pkg.qualifiedName()} {\n\n") + } + + override fun afterVisitPackage(pkg: PackageItem) { + writer.print("}\n\n") + } + + override fun visitConstructor(constructor: ConstructorItem) { + writer.print(" ctor ") + writeModifiers(constructor) + // Note - we don't write out the type parameter list (constructor.typeParameterList()) in signature files! + //writeTypeParameterList(constructor.typeParameterList(), addSpace = true) + writer.print(constructor.containingClass().fullName()) + writeParameterList(constructor) + writeThrowsList(constructor) + writer.print(";\n") + } + + override fun visitField(field: FieldItem) { + val name = if (field.isEnumConstant()) "enum_constant" else "field" + writer.print(" ") + writer.print(name) + writer.print(" ") + writeModifiers(field) + writeType(field.type(), field.modifiers) + writer.print(' ') + writer.print(field.name()) + field.writeValueWithSemicolon(writer, allowDefaultValue = false, requireInitialValue = false) + writer.print("\n") + } + + override fun visitMethod(method: MethodItem) { + if (compatibility.skipAnnotationInstanceMethods && method.containingClass().isAnnotationType() && + !method.modifiers.isStatic() + ) { + return + } + + if (compatibility.skipInheritedInterfaceMethods && method.inheritedInterfaceMethod) { + return + } + + writer.print(" method ") + writeModifiers(method) + writeTypeParameterList(method.typeParameterList(), addSpace = true) + + writeType(method.returnType(), method.modifiers) + writer.print(' ') + writer.print(method.name()) + writeParameterList(method) + writeThrowsList(method) + writer.print(";\n") + } + + override fun visitClass(cls: ClassItem) { + writer.print(" ") + + if (compatibility.extraSpaceForEmptyModifiers && cls.isPackagePrivate && cls.isPackagePrivate) { + writer.print(" ") + } + + writeModifiers(cls) + + if (cls.isAnnotationType()) { + if (compatibility.classForAnnotations) { + // doclava incorrectly treats annotations (such as TargetApi) as an abstract class instead + // of an @interface! + // + // Example: + // public abstract class SuppressLint implements java.lang.annotation.Annotation { } + writer.print("class") + } else { + writer.print("@interface") + } + } else if (cls.isInterface()) { + writer.print("interface") + } else if (!compatibility.classForEnums && cls.isEnum()) { // compat mode calls enums "class" instead + writer.print("enum") + } else { + writer.print("class") + } + writer.print(" ") + writer.print(cls.fullName()) + writeTypeParameterList(cls.typeParameterList(), addSpace = false) + writeSuperClassStatement(cls) + writeInterfaceList(cls) + + writer.print(" {\n") + } + + override fun afterVisitClass(cls: ClassItem) { + writer.print(" }\n\n") + } + + private fun writeModifiers(item: Item) { + ModifierList.write( + writer = writer, + modifiers = item.modifiers, + item = item, + includeDeprecated = true, + includeAnnotations = compatibility.annotationsInSignatures, + skipNullnessAnnotations = options.outputKotlinStyleNulls, + omitCommonPackages = options.omitCommonPackages + ) + } + + private fun writeSuperClassStatement(cls: ClassItem) { + if (!compatibility.classForEnums && cls.isEnum() || cls.isAnnotationType()) { + return + } + + if (cls.isInterface() && compatibility.extendsForInterfaceSuperClass) { + // Written in the interface section instead + return + } + + val superClass = if (preFiltered) + cls.superClassType() + else + cls.filteredSuperClassType(filterReference) + if (superClass != null && !superClass.isJavaLangObject()) { + val superClassString = + superClass.toTypeString(erased = compatibility.omitTypeParametersInInterfaces) + writer.print(" extends ") + writer.print(superClassString) + } + } + + private fun writeInterfaceList(cls: ClassItem) { + if (cls.isAnnotationType()) { + if (compatibility.classForAnnotations) { + writer.print(" implements java.lang.annotation.Annotation") + } + return + } + val isInterface = cls.isInterface() + + val interfaces = if (preFiltered) + cls.interfaceTypes().asSequence() + else + cls.filteredInterfaceTypes(filterReference).asSequence() + val all: Sequence<TypeItem> = if (isInterface && compatibility.extendsForInterfaceSuperClass) { + val superClassType = cls.superClassType() + if (superClassType != null && !superClassType.isJavaLangObject()) { + interfaces.plus(sequenceOf(superClassType)) + } else { + interfaces + } + } else { + interfaces + } + + if (all.any()) { + val label = if (isInterface && !compatibility.extendsForInterfaceSuperClass) " extends" else " implements" + writer.print(label) + + all.sortedWith(TypeItem.comparator).forEach { item -> + writer.print(" ") + writer.print(item.toTypeString(erased = compatibility.omitTypeParametersInInterfaces)) + } + } + } + + private fun writeTypeParameterList(typeList: String?, addSpace: Boolean) { + if (typeList != null) { + writer.print(typeList) + if (addSpace) { + writer.print(' ') + } + } + } + + private fun writeParameterList(method: MethodItem) { + writer.print("(") + val emitParameterNames = compatibility.parameterNames + method.parameters().asSequence().forEachIndexed { i, parameter -> + if (i > 0) { + writer.print(", ") + } + writeModifiers(parameter) + writeType(parameter.type(), parameter.modifiers) + if (emitParameterNames) { + val name = parameter.publicName() + if (name != null) { + writer.print(" ") + writer.print(name) + } + } + } + writer.print(")") + } + + private fun writeType(type: TypeItem?, modifiers: ModifierList) { + type ?: return + + var typeString = type.toTypeString( + erased = false, + outerAnnotations = false, + innerAnnotations = compatibility.annotationsInSignatures + ) + + // Strip java.lang. prefix? + if (options.omitCommonPackages) { + typeString = TypeItem.shortenTypes(typeString) + } + + writer.print(typeString) + + if (options.outputKotlinStyleNulls && !type.primitive) { + var nullable: Boolean? = null + for (annotation in modifiers.annotations()) { + if (annotation.isNullable()) { + nullable = true + } else if (annotation.isNonNull()) { + nullable = false + } + } + when (nullable) { + null -> writer.write("!") + true -> writer.write("?") + // else: non-null: nothing to write + } + } + } + + private fun writeThrowsList(method: MethodItem) { + val throws = if (preFiltered) + method.throwsTypes().asSequence().sortedWith(ClassItem.fullNameComparator) + else + method.throwsTypes().asSequence() + .filter { filterReference.test(it) } + .sortedWith(ClassItem.fullNameComparator) + if (throws.any()) { + writer.print(" throws ") + throws.asSequence().forEachIndexed { i, type -> + if (i > 0) { + writer.print(", ") + } + writer.print(type.qualifiedName()) + } + } + } +} \ 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 new file mode 100644 index 0000000..0cf793e --- /dev/null +++ b/src/main/java/com/android/tools/metalava/StubWriter.kt @@ -0,0 +1,516 @@ +/* + * Copyright (C) 2017 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.doclava1.ApiPredicate +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.doclava1.FilterPredicate +import com.android.tools.metalava.model.ClassItem +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.MethodItem +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.psi.PsiClassItem +import com.android.tools.metalava.model.psi.trimDocIndent +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.google.common.io.Files +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.PrintWriter +import kotlin.text.Charsets.UTF_8 + +class StubWriter( + codebase: Codebase, + private val stubsDir: File, + private val generateAnnotations: Boolean = false, + private val preFiltered: Boolean = true +) : ApiVisitor( + visitConstructorsAsMethods = false, + nestInnerClasses = true, + inlineInheritedFields = true, + fieldComparator = FieldItem.comparator, + // Methods are by default sorted in source order in stubs, to encourage methods + // that are near each other in the source to show up near each other in the documentation + methodComparator = MethodItem.sourceOrderComparator, + filterEmit = FilterPredicate(ApiPredicate(codebase)), + filterReference = ApiPredicate(codebase, ignoreShown = true) +) { + + private val sourceList = StringBuilder(20000) + + override fun include(cls: ClassItem): Boolean { + val filter = options.stubPackages + if (filter != null && !filter.matches(cls.containingPackage())) { + return false + } + return super.include(cls) + } + + /** Writes a source file list of the generated stubs */ + fun writeSourceList(target: File, root: File?) { + target.parentFile?.mkdirs() + val contents = if (root != null) { + val path = root.path.replace('\\', '/') + sourceList.toString().replace(path, "") + } else { + sourceList.toString() + } + Files.asCharSink(target, UTF_8).write(contents) + } + + private fun startFile(sourceFile: File) { + if (sourceList.isNotEmpty()) { + sourceList.append(' ') + } + sourceList.append(sourceFile.path.replace('\\', '/')) + } + + override fun visitPackage(pkg: PackageItem) { + getPackageDir(pkg, create = true) + + // TODO: Write package annotations into package-info.java! + // TODO: Write package.html, if applicable + } + + private fun getPackageDir(packageItem: PackageItem, create: Boolean = true): File { + val relative = packageItem.qualifiedName().replace('.', File.separatorChar) + val dir = File(stubsDir, relative) + if (create && !dir.isDirectory) { + val ok = dir.mkdirs() + if (!ok) { + throw IOException("Could not create $dir") + } + } + + return dir + } + + private fun getClassFile(classItem: ClassItem): File { + assert(classItem.containingClass() == null, { "Should only be called on top level classes" }) + // TODO: Look up compilation unit language + return File(getPackageDir(classItem.containingPackage()), "${classItem.simpleName()}.java") + } + + /** + * Between top level class files the [writer] field doesn't point to a real file; it + * points to this writer, which redirects to the error output. Nothing should be written + * to the writer at that time. + */ + private var errorWriter = PrintWriter(options.stderr) + + /** The writer to write the stubs file to */ + private var writer: PrintWriter = errorWriter + + override fun visitClass(cls: ClassItem) { + if (cls.isTopLevelClass()) { + val sourceFile = getClassFile(cls) + writer = try { + PrintWriter(BufferedWriter(FileWriter(sourceFile))) + } catch (e: IOException) { + reporter.report(Errors.IO_ERROR, sourceFile, "Cannot open file for write.") + errorWriter + } + + startFile(sourceFile) + + // Copyright statements from the original file? + val compilationUnit = cls.getCompilationUnit() + compilationUnit?.getHeaderComments()?.let { writer.println(it) } + + val qualifiedName = cls.containingPackage().qualifiedName() + if (qualifiedName.isNotBlank()) { + writer.println("package $qualifiedName;") + writer.println() + } + + compilationUnit?.getImportStatements(filterReference)?.let { + for (importedClass in it) { + writer.println("import $importedClass;") + } + writer.println() + } + } + + appendDocumentation(cls, writer) + + // "ALL" doesn't do it; compiler still warns unless you actually explicitly list "unchecked" + writer.println("@SuppressWarnings({\"unchecked\", \"deprecation\", \"all\"})") + + // Need to filter out abstract from the modifiers list and turn it + // into a concrete method to make the stub compile + val removeAbstract = cls.modifiers.isAbstract() && (cls.isEnum() || cls.isAnnotationType()) + + appendModifiers(cls, removeAbstract) + + when { + cls.isAnnotationType() -> writer.print("@interface") + cls.isInterface() -> writer.print("interface") + cls.isEnum() -> writer.print("enum") + else -> writer.print("class") + } + + writer.print(" ") + writer.print(cls.simpleName()) + + generateTypeParameterList(typeList = cls.typeParameterList(), addSpace = false) + generateSuperClassStatement(cls) + generateInterfaceList(cls) + + writer.print(" {\n") + + if (cls.isEnum()) { + var first = true + // Enums should preserve the original source order, not alphabetical etc sort + for (field in cls.fields().sortedBy { it.sortingRank }) { + if (field.isEnumConstant()) { + if (first) { + first = false + } else { + writer.write(", ") + } + writer.write(field.name()) + } + } + writer.println(";") + } + + generateMissingConstructors(cls) + } + + private fun appendDocumentation(item: Item, writer: PrintWriter) { + val documentation = item.fullyQualifiedDocumentation() + if (documentation.isNotBlank()) { + val trimmed = trimDocIndent(documentation) + writer.println(trimmed) + writer.println() + } + } + + override fun afterVisitClass(cls: ClassItem) { + writer.print("}\n\n") + + if (cls.isTopLevelClass()) { + writer.flush() + writer.close() + writer = errorWriter + } + } + + private fun appendModifiers( + item: Item, + removeAbstract: Boolean, + removeFinal: Boolean = false, + addPublic: Boolean = false + ) { + appendModifiers(item, item.modifiers, removeAbstract, removeFinal, addPublic) + } + + private fun appendModifiers( + item: Item, + modifiers: ModifierList, + removeAbstract: Boolean, + removeFinal: Boolean = false, + addPublic: Boolean = false + ) { + if (item.deprecated) { + writer.write("@Deprecated ") + } + + ModifierList.write( + writer, modifiers, item, removeAbstract = removeAbstract, removeFinal = removeFinal, + addPublic = addPublic, includeAnnotations = generateAnnotations + ) + } + + private fun generateSuperClassStatement(cls: ClassItem) { + if (cls.isEnum() || cls.isAnnotationType()) { + // No extends statement for enums and annotations; it's implied by the "enum" and "@interface" keywords + return + } + + val superClass = if (preFiltered) + cls.superClassType() + else + cls.filteredSuperClassType(filterReference) + + + if (superClass != null && !superClass.isJavaLangObject()) { + val qualifiedName = superClass.toTypeString() + writer.print(" extends ") + + if (qualifiedName.contains("<")) { + // TODO: I need to push this into the model at filter-time such that clients don't need + // to remember to do this!! + val s = superClass.asClass() + if (s != null) { + val map = cls.mapTypeVariables(s) + val replaced = superClass.convertTypeString(map) + writer.print(replaced) + return + } + } + (cls as PsiClassItem).psiClass.superClassType + writer.print(qualifiedName) + } + } + + private fun generateInterfaceList(cls: ClassItem) { + if (cls.isAnnotationType()) { + // No extends statement for annotations; it's implied by the "@interface" keyword + return + } + + val interfaces = if (preFiltered) + cls.interfaceTypes().asSequence() + else + cls.filteredInterfaceTypes(filterReference).asSequence() + + if (interfaces.any()) { + if (cls.isInterface() && cls.superClassType() != null) + writer.print(", ") + else + writer.print(" implements") + interfaces.forEachIndexed { index, type -> + if (index > 0) { + writer.print(",") + } + writer.print(" ") + writer.print(type.toTypeString()) + } + } else if (compatibility.classForAnnotations && cls.isAnnotationType()) { + writer.print(" implements java.lang.annotation.Annotation") + } + } + + private fun generateTypeParameterList( + typeList: String?, + addSpace: Boolean + ) { + // TODO: Do I need to map type variables? + + if (typeList != null) { + writer.print(typeList) + + if (addSpace) { + writer.print(' ') + } + } + } + + override fun visitConstructor(constructor: ConstructorItem) { + writeConstructor(constructor, constructor.superConstructor) + } + + private fun writeConstructor( + constructor: MethodItem, + superConstructor: MethodItem? + ) { + writer.println() + appendDocumentation(constructor, writer) + appendModifiers(constructor, false) + generateTypeParameterList( + typeList = constructor.typeParameterList(), + addSpace = true + ) + writer.print(constructor.containingClass().simpleName()) + + generateParameterList(constructor) + generateThrowsList(constructor) + + writer.print(" { ") + + writeConstructorBody(constructor, superConstructor) + writer.println(" }") + } + + private fun writeConstructorBody(constructor: MethodItem?, superConstructor: MethodItem?) { + // Find any constructor in parent that we can compile against + superConstructor?.let { it -> + val parameters = it.parameters() + val invokeOnThis = constructor != null && constructor.containingClass() == it.containingClass() + if (invokeOnThis || parameters.isNotEmpty()) { + val includeCasts = parameters.isNotEmpty() && + it.containingClass().constructors().filter { filterReference.test(it) }.size > 1 + if (invokeOnThis) { + writer.print("this(") + } else { + writer.print("super(") + } + parameters.forEachIndexed { index, parameter -> + if (index > 0) { + writer.write(", ") + } + val type = parameter.type() + val typeString = type.toErasedTypeString() + if (!type.primitive) { + if (includeCasts) { + writer.write("(") + + // Types with varargs can't appear as varargs when used as an argument + if (typeString.contains("...")) { + writer.write(typeString.replace("...", "[]")) + } else { + writer.write(typeString) + } + writer.write(")") + } + writer.write("null") + + } else { + if (typeString != "boolean" && typeString != "int" && typeString != "long") { + writer.write("(") + writer.write(typeString) + writer.write(")") + } + writer.write(type.defaultValueString()) + } + } + writer.print("); ") + } + } + + writeThrowStub() + } + + private fun generateMissingConstructors(cls: ClassItem) { + val clsDefaultConstructor = cls.defaultConstructor + val constructors = cls.filteredConstructors(filterEmit) + if (clsDefaultConstructor != null && !constructors.contains(clsDefaultConstructor)) { + clsDefaultConstructor.mutableModifiers().setPackagePrivate(true) + visitConstructor(clsDefaultConstructor) + return + } + } + + override fun visitMethod(method: MethodItem) { + writeMethod(method.containingClass(), method, false) + } + + private fun writeMethod(containingClass: ClassItem, method: MethodItem, movedFromInterface: Boolean) { + val modifiers = method.modifiers + val isEnum = containingClass.isEnum() + val isAnnotation = containingClass.isAnnotationType() + + if (isEnum && (method.name() == "values" || + method.name() == "valueOf" && method.parameters().size == 1 && + 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 + // so inserted in compat mode. + return + } + + writer.println() + appendDocumentation(method, writer) + + // Need to filter out abstract from the modifiers list and turn it + // into a concrete method to make the stub compile + val removeAbstract = modifiers.isAbstract() && (isEnum || isAnnotation) || movedFromInterface + + appendModifiers(method, modifiers, removeAbstract, movedFromInterface) + generateTypeParameterList(typeList = method.typeParameterList(), addSpace = true) + + val returnType = method.returnType() + writer.print(returnType?.toTypeString(outerAnnotations = false, innerAnnotations = true)) + + writer.print(' ') + writer.print(method.name()) + generateParameterList(method) + generateThrowsList(method) + + if (modifiers.isAbstract() && !removeAbstract && !isEnum || isAnnotation || modifiers.isNative()) { + writer.println(";") + } else { + writer.print(" { ") + writeThrowStub() + writer.println(" }") + } + } + + override fun visitField(field: FieldItem) { + // Handled earlier in visitClass + if (field.isEnumConstant()) { + return + } + + writer.println() + + appendDocumentation(field, writer) + appendModifiers(field, false, false) + writer.print(field.type().toTypeString(outerAnnotations = false, innerAnnotations = true)) + writer.print(' ') + writer.print(field.name()) + val needsInitialization = + field.modifiers.isFinal() && field.initialValue(true) == null && field.containingClass().isClass() + field.writeValueWithSemicolon( + writer, + allowDefaultValue = !needsInitialization, + requireInitialValue = !needsInitialization + ) + writer.print("\n") + + if (needsInitialization) { + if (field.modifiers.isStatic()) { + writer.print("static ") + } + writer.print("{ ${field.name()} = ${field.type().defaultValueString()}; }\n") + } + } + + private fun writeThrowStub() { + writer.write("throw new RuntimeException(\"Stub!\");") + } + + private fun generateParameterList(method: MethodItem) { + writer.print("(") + method.parameters().asSequence().forEachIndexed { i, parameter -> + if (i > 0) { + writer.print(", ") + } + appendModifiers(parameter, false) + writer.print(parameter.type().toTypeString(outerAnnotations = false, innerAnnotations = true)) + writer.print(' ') + val name = parameter.publicName() ?: parameter.name() + writer.print(name) + } + writer.print(")") + } + + private fun generateThrowsList(method: MethodItem) { + // Note that throws types are already sorted internally to help comparison matching + val throws = if (preFiltered) + method.throwsTypes().asSequence() + else + method.throwsTypes().asSequence().filter { filterReference.test(it) } + if (throws.any()) { + writer.print(" throws ") + throws.asSequence().sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type -> + if (i > 0) { + writer.print(", ") + } + // TODO: Shouldn't declare raw types here! + writer.print(type.qualifiedName()) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/Terminal.kt b/src/main/java/com/android/tools/metalava/Terminal.kt new file mode 100644 index 0000000..8a32691 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Terminal.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 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 java.io.PrintWriter + +enum class TerminalColor(val value: Int) { + BLACK(0), + RED(1), + GREEN(2), + YELLOW(3), + BLUE(4), + MAGENTA(5), + CYAN(6), + WHITE(7) +} + +fun terminalAttributes( + bold: Boolean = false, underline: Boolean = false, reverse: Boolean = false, + foreground: TerminalColor? = null, background: TerminalColor? = null +): String { + val sb = StringBuilder() + sb.append("\u001B[") + if (foreground != null) { + sb.append('3').append('0' + foreground.value) + } + if (background != null) { + if (sb.last().isDigit()) + sb.append(';') + sb.append('4').append('0' + background.value) + } + + if (bold) { + if (sb.last().isDigit()) + sb.append(';') + sb.append('1') + } + if (underline) { + if (sb.last().isDigit()) + sb.append(';') + sb.append('4') + } + if (reverse) { + if (sb.last().isDigit()) + sb.append(';') + sb.append('7') + } + if (sb.last() == '[') { + // Nothing: Reset + sb.append('0') + } + sb.append("m") + return sb.toString() +} + +fun resetTerminal(): String { + return "\u001b[0m" +} + +fun colorized(string: String, color: TerminalColor): String { + return "${terminalAttributes(foreground = color)}$string${resetTerminal()}" +} + +fun bold(string: String): String { + return "${terminalAttributes(bold = true)}$string${resetTerminal()}" +} + +fun PrintWriter.terminalPrint( + string: String, + bold: Boolean = false, underline: Boolean = false, reverse: Boolean = false, + foreground: TerminalColor? = null, background: TerminalColor? = null +) { + print( + terminalAttributes( + bold = bold, underline = underline, reverse = reverse, foreground = foreground, + background = background + ) + ) + print(string) + print(resetTerminal()) +} diff --git a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java new file mode 100644 index 0000000..378d536 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017 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.apilevels; + +import com.android.annotations.NonNull; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Reads all the android.jar files found in an SDK and generate a map of {@link ApiClass}. + */ +public class AndroidJarReader { + private int mMinApi; + private int mCurrentApi; + private File mCurrentJar; + private List<String> mPatterns; + private File[] mApiLevels; + + AndroidJarReader(@NonNull List<String> patterns, int minApi, @NonNull File currentJar, int currentApi) { + mPatterns = patterns; + mMinApi = minApi; + mCurrentJar = currentJar; + mCurrentApi = currentApi; + } + + AndroidJarReader(@NonNull File[] apiLevels) { + mApiLevels = apiLevels; + } + + public Api getApi() throws IOException { + Api api = new Api(); + if (mApiLevels != null) { + for (int apiLevel = 1; apiLevel < mApiLevels.length; apiLevel++) { + File jar = getAndroidJarFile(apiLevel); + readJar(api, apiLevel, jar); + } + } else { + // Get all the android.jar. They are in platforms-# + int apiLevel = mMinApi - 1; + while (true) { + apiLevel++; + File jar = null; + if (apiLevel == mCurrentApi) { + jar = mCurrentJar; + } + if (jar == null) { + jar = getAndroidJarFile(apiLevel); + } + if (jar == null || !jar.isFile()) { + System.out.println("Last API level found: " + (apiLevel - 1)); + break; + } + System.out.println("Found API " + apiLevel + " at " + jar.getPath()); + readJar(api, apiLevel, jar); + } + } + + api.removeImplicitInterfaces(); + api.removeOverridingMethods(); + + return api; + } + + private void readJar(Api api, int apiLevel, File jar) throws IOException { + api.update(apiLevel); + + FileInputStream fis = new FileInputStream(jar); + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + String name = entry.getName(); + + if (name.endsWith(".class")) { + byte[] bytes = ByteStreams.toByteArray(zis); + if (bytes == null) { + System.err.println("Warning: Couldn't read " + name); + entry = zis.getNextEntry(); + continue; + } + + ClassReader reader = new ClassReader(bytes); + ClassNode classNode = new ClassNode(Opcodes.ASM5); + reader.accept(classNode, 0 /*flags*/); + + ApiClass theClass = api.addClass(classNode.name, apiLevel, + (classNode.access & Opcodes.ACC_DEPRECATED) != 0); + + // super class + if (classNode.superName != null) { + theClass.addSuperClass(classNode.superName, apiLevel); + } + + // interfaces + for (Object interfaceName : classNode.interfaces) { + theClass.addInterface((String) interfaceName, apiLevel); + } + + // fields + for (Object field : classNode.fields) { + FieldNode fieldNode = (FieldNode) field; + if ((fieldNode.access & Opcodes.ACC_PRIVATE) != 0) { + continue; + } + if (!fieldNode.name.startsWith("this$") && + !fieldNode.name.equals("$VALUES")) { + boolean deprecated = (fieldNode.access & Opcodes.ACC_DEPRECATED) != 0; + theClass.addField(fieldNode.name, apiLevel, deprecated); + } + } + + // methods + for (Object method : classNode.methods) { + MethodNode methodNode = (MethodNode) method; + if ((methodNode.access & Opcodes.ACC_PRIVATE) != 0) { + continue; + } + if (!methodNode.name.equals("<clinit>")) { + boolean deprecated = (methodNode.access & Opcodes.ACC_DEPRECATED) != 0; + theClass.addMethod(methodNode.name + methodNode.desc, apiLevel, deprecated); + } + } + } + entry = zis.getNextEntry(); + } + + Closeables.close(fis, true); + } + + private File getAndroidJarFile(int apiLevel) { + if (mApiLevels != null) { + return mApiLevels[apiLevel]; + } + for (String pattern : mPatterns) { + File f = new File(pattern.replace("%", Integer.toString(apiLevel))); + if (f.isFile()) { + return f; + } + } + return null; + } +} diff --git a/src/main/java/com/android/tools/metalava/apilevels/Api.java b/src/main/java/com/android/tools/metalava/apilevels/Api.java new file mode 100644 index 0000000..30dc5f7 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/apilevels/Api.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 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.apilevels; + +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the whole Android API. + */ +public class Api extends ApiElement { + private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>(); + + public Api() { + // Pretend that API started from version 0 to make sure that classes existed in the first version + // are printed with since="1". + super("Android API"); + } + + /** + * Prints the whole API definition to a stream. + * + * @param stream the stream to print the XML elements to + */ + public void print(PrintStream stream) { + stream.println("<api version=\"2\">"); + print(mClasses.values(), "class", "\t", stream); + printClosingTag("api", "", stream); + } + + /** + * Adds or updates a class. + * + * @param name the name of the class + * @param version an API version in which the class existed + * @param deprecated whether the class was deprecated in the API version + * @return the newly created or a previously existed class + */ + public ApiClass addClass(String name, int version, boolean deprecated) { + ApiClass classElement = mClasses.get(name); + if (classElement == null) { + classElement = new ApiClass(name, version, deprecated); + mClasses.put(name, classElement); + } else { + classElement.update(version, deprecated); + } + return classElement; + } + + /** + * The bytecode visitor registers interfaces listed for a class. However, + * a class will <b>also</b> implement interfaces implemented by the super classes. + * This isn't available in the class file, so after all classes have been read in, + * we iterate through all classes, and for those that have interfaces, we check up + * the inheritance chain to see if it has already been introduced in a super class + * at an earlier API level. + */ + public void removeImplicitInterfaces() { + for (ApiClass classElement : mClasses.values()) { + classElement.removeImplicitInterfaces(mClasses); + } + } + + /** + * @see ApiClass#removeOverridingMethods + */ + public void removeOverridingMethods() { + for (ApiClass classElement : mClasses.values()) { + classElement.removeOverridingMethods(mClasses); + } + } +} diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java new file mode 100644 index 0000000..a9bcc19 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2017 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.apilevels; + +import com.google.common.collect.Iterables; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Represents a class or an interface and its methods/fields. + * This is used to write the simplified XML file containing all the public API. + */ +public class ApiClass extends ApiElement { + private final List<ApiElement> mSuperClasses = new ArrayList<ApiElement>(); + private final List<ApiElement> mInterfaces = new ArrayList<ApiElement>(); + + private final Map<String, ApiElement> mFields = new HashMap<String, ApiElement>(); + private final Map<String, ApiElement> mMethods = new HashMap<String, ApiElement>(); + + public ApiClass(String name, int version, boolean deprecated) { + super(name, version, deprecated); + } + + public void addField(String name, int version, boolean deprecated) { + addToMap(mFields, name, version, deprecated); + } + + public void addMethod(String name, int version, boolean deprecated) { + addToMap(mMethods, name, version, deprecated); + } + + public void addSuperClass(String superClass, int since) { + addToArray(mSuperClasses, superClass, since); + } + + public void addInterface(String interfaceClass, int since) { + addToArray(mInterfaces, interfaceClass, since); + } + + private void addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) { + ApiElement element = elements.get(name); + if (element == null) { + element = new ApiElement(name, version, deprecated); + elements.put(name, element); + } else { + element.update(version, deprecated); + } + } + + private void addToArray(Collection<ApiElement> elements, String name, int version) { + ApiElement element = findByName(elements, name); + if (element == null) { + element = new ApiElement(name, version); + elements.add(element); + } else { + element.update(version); + } + } + + private ApiElement findByName(Collection<ApiElement> collection, String name) { + for (ApiElement element : collection) { + if (element.getName().equals(name)) { + return element; + } + } + return null; + } + + @Override + public void print(String tag, ApiElement parentElement, String indent, PrintStream stream) { + super.print(tag, false, parentElement, indent, stream); + String innerIndent = indent + '\t'; + print(mSuperClasses, "extends", innerIndent, stream); + print(mInterfaces, "implements", innerIndent, stream); + print(mMethods.values(), "method", innerIndent, stream); + print(mFields.values(), "field", innerIndent, stream); + printClosingTag(tag, indent, stream); + } + + /** + * Removes all interfaces that are also implemented by superclasses or extended by interfaces + * this class implements. + * + * @param allClasses all classes keyed by their names. + */ + public void removeImplicitInterfaces(Map<String, ApiClass> allClasses) { + if (mInterfaces.isEmpty() || mSuperClasses.isEmpty()) { + return; + } + + for (Iterator<ApiElement> iterator = mInterfaces.iterator(); iterator.hasNext(); ) { + ApiElement interfaceElement = iterator.next(); + + for (ApiElement superClass : mSuperClasses) { + if (superClass.introducedNotLaterThan(interfaceElement)) { + ApiClass cls = allClasses.get(superClass.getName()); + if (cls != null && cls.implementsInterface(interfaceElement, allClasses)) { + iterator.remove(); + break; + } + } + } + } + } + + private boolean implementsInterface(ApiElement interfaceElement, Map<String, ApiClass> allClasses) { + for (ApiElement localInterface : mInterfaces) { + if (localInterface.introducedNotLaterThan(interfaceElement)) { + if (interfaceElement.getName().equals(localInterface.getName())) { + return true; + } + // Check parent interface. + ApiClass cls = allClasses.get(localInterface.getName()); + if (cls != null && cls.implementsInterface(interfaceElement, allClasses)) { + return true; + } + } + } + + for (ApiElement superClass : mSuperClasses) { + if (superClass.introducedNotLaterThan(interfaceElement)) { + ApiClass cls = allClasses.get(superClass.getName()); + if (cls != null && cls.implementsInterface(interfaceElement, allClasses)) { + return true; + } + } + } + return false; + } + + /** + * Removes all methods that override method declared by superclasses and interfaces of this class. + * + * @param allClasses all classes keyed by their names. + */ + public void removeOverridingMethods(Map<String, ApiClass> allClasses) { + for (Iterator<Map.Entry<String, ApiElement>> iter = mMethods.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry<String, ApiElement> entry = iter.next(); + ApiElement method = entry.getValue(); + if (!method.getName().startsWith("<init>(") && isOverrideOfInherited(method, allClasses)) { + iter.remove(); + } + } + } + + /** + * Checks if the given method overrides one of the methods defined by this class or + * its superclasses or interfaces. + * + * @param method the method to check + * @param allClasses the map containing all API classes + * @return true if the method is an override + */ + private boolean isOverride(ApiElement method, Map<String, ApiClass> allClasses) { + ApiElement localMethod = mMethods.get(method.getName()); + if (localMethod != null && localMethod.introducedNotLaterThan(method)) { + // This class has the method and it was introduced in at the same api level + // as the child method, or before. + return true; + } + return isOverrideOfInherited(method, allClasses); + } + + /** + * Checks if the given method overrides one of the methods declared by ancestors of this class. + */ + private boolean isOverrideOfInherited(ApiElement method, Map<String, ApiClass> allClasses) { + // Check this class' parents. + for (ApiElement parent : Iterables.concat(mSuperClasses, mInterfaces)) { + // Only check the parent if it was a parent class at the introduction of the method. + if (parent.introducedNotLaterThan(method)) { + ApiClass cls = allClasses.get(parent.getName()); + if (cls != null && cls.isOverride(method, allClasses)) { + return true; + } + } + } + + return false; + } + + @Override + public String toString() { + return getName(); + } +} diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java new file mode 100644 index 0000000..ead821c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 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.apilevels; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Represents an API element, e.g. class, method or field. + */ +public class ApiElement implements Comparable<ApiElement> { + private final String mName; + private int mSince; + private int mDeprecatedIn; + private int mLastPresentIn; + + /** + * @param name the name of the API element + * @param version an API version for which the API element existed + * @param deprecated whether the API element was deprecated in the API version in question + */ + public ApiElement(String name, int version, boolean deprecated) { + assert name != null; + assert version > 0; + mName = name; + mSince = version; + mLastPresentIn = version; + if (deprecated) { + mDeprecatedIn = version; + } + } + + /** + * @param name the name of the API element + * @param version an API version for which the API element existed + */ + public ApiElement(String name, int version) { + this(name, version, false); + } + + protected ApiElement(String name) { + assert name != null; + mName = name; + } + + /** + * Returns the name of the API element. + */ + public final String getName() { + return mName; + } + + /** + * Checks if this API element was introduced not later than another API element. + * + * @param other the API element to compare to + * @return true if this API element was introduced not later than {@code other} + */ + public final boolean introducedNotLaterThan(ApiElement other) { + return mSince <= other.mSince; + } + + /** + * Updates the API element with information for a specific API version. + * + * @param version an API version for which the API element existed + * @param deprecated whether the API element was deprecated in the API version in question + */ + public void update(int version, boolean deprecated) { + assert version > 0; + if (mSince > version) { + mSince = version; + } + if (mLastPresentIn < version) { + mLastPresentIn = version; + } + if (deprecated) { + if (mDeprecatedIn == 0 || mDeprecatedIn > version) { + mDeprecatedIn = version; + } + } + } + + /** + * Updates the API element with information for a specific API version. + * + * @param version an API version for which the API element existed + */ + public void update(int version) { + update(version, false); + } + + /** + * Checks whether the API element is deprecated or not. + */ + public final boolean isDeprecated() { + return mDeprecatedIn != 0; + } + + /** + * Prints an XML representation of the element to a stream terminated by a line break. + * Attributes with values matching the parent API element are omitted. + * + * @param tag the tag of the XML element + * @param parentElement the parent API element + * @param indent the whitespace prefix to insert before the XML element + * @param stream the stream to print the XML element to + */ + public void print(String tag, ApiElement parentElement, String indent, PrintStream stream) { + print(tag, true, parentElement, indent, stream); + } + + /** + * Prints an XML representation of the element to a stream terminated by a line break. + * Attributes with values matching the parent API element are omitted. + * + * @param tag the tag of the XML element + * @param closeTag if true the XML element is terminated by "/>", otherwise the closing + * tag of the element is not printed + * @param parentElement the parent API element + * @param indent the whitespace prefix to insert before the XML element + * @param stream the stream to print the XML element to + * @see #printClosingTag(String, String, PrintStream) + */ + protected void print(String tag, boolean closeTag, ApiElement parentElement, String indent, + PrintStream stream) { + stream.print(indent); + stream.print('<'); + stream.print(tag); + stream.print(" name=\""); + stream.print(encodeAttribute(mName)); + if (mSince > parentElement.mSince) { + stream.print("\" since=\""); + stream.print(mSince); + } + if (mDeprecatedIn != 0) { + stream.print("\" deprecated=\""); + stream.print(mDeprecatedIn); + } + if (mLastPresentIn < parentElement.mLastPresentIn) { + stream.print("\" removed=\""); + stream.print(mLastPresentIn + 1); + } + stream.print('"'); + if (closeTag) { + stream.print('/'); + } + stream.println('>'); + } + + /** + * Prints homogeneous XML elements to a stream. Each element is printed on a separate line. + * Attributes with values matching the parent API element are omitted. + * + * @param elements the elements to print + * @param tag the tag of the XML elements + * @param indent the whitespace prefix to insert before each XML element + * @param stream the stream to print the XML elements to + */ + protected void print(Collection<? extends ApiElement> elements, String tag, String indent, PrintStream stream) { + for (ApiElement element : sortedList(elements)) { + element.print(tag, this, indent, stream); + } + } + + private <T extends ApiElement> List<T> sortedList(Collection<T> elements) { + List<T> list = new ArrayList<T>(elements); + Collections.sort(list); + return list; + } + + /** + * Prints a closing tag of an XML element terminated by a line break. + * + * @param tag the tag of the element + * @param indent the whitespace prefix to insert before the closing tag + * @param stream the stream to print the XML element to + */ + protected static void printClosingTag(String tag, String indent, PrintStream stream) { + stream.print(indent); + stream.print("</"); + stream.print(tag); + stream.println('>'); + } + + protected static String encodeAttribute(String attribute) { + StringBuilder sb = new StringBuilder(); + int n = attribute.length(); + // &, ", ' and < are illegal in attributes; see http://www.w3.org/TR/REC-xml/#NT-AttValue + // (' legal in a " string and " is legal in a ' string but here we'll stay on the safe side). + for (int i = 0; i < n; i++) { + char c = attribute.charAt(i); + if (c == '"') { + sb.append("""); //$NON-NLS-1$ + } else if (c == '<') { + sb.append("<"); //$NON-NLS-1$ + } else if (c == '\'') { + sb.append("'"); //$NON-NLS-1$ + } else if (c == '&') { + sb.append("&"); //$NON-NLS-1$ + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + @Override + public int compareTo(ApiElement other) { + return mName.compareTo(other.mName); + } +} diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java new file mode 100644 index 0000000..e3b3225 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2017 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.apilevels; + +import com.android.annotations.NonNull; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Main class for command line command to convert the existing API XML/TXT files into diff-based + * simple text files. + */ +public class ApiGenerator { + public static void main(String[] args) { + boolean error = false; + int minApi = 1; + int currentApi = -1; + String currentCodename = null; + File currentJar = null; + List<String> patterns = new ArrayList<>(); + String outPath = null; + + for (int i = 0; i < args.length && !error; i++) { + String arg = args[i]; + + if (arg.equals("--pattern")) { + i++; + if (i < args.length) { + patterns.add(args[i]); + } else { + System.err.println("Missing argument after " + arg); + error = true; + } + } else if (arg.equals("--current-version")) { + i++; + if (i < args.length) { + currentApi = Integer.parseInt(args[i]); + if (currentApi <= 22) { + System.err.println("Suspicious currentApi=" + currentApi + ", expected at least 23"); + error = true; + } + } else { + System.err.println("Missing number >= 1 after " + arg); + error = true; + } + } else if (arg.equals("--current-codename")) { + i++; + if (i < args.length) { + currentCodename = args[i]; + } else { + System.err.println("Missing codename after " + arg); + error = true; + } + } else if (arg.equals("--current-jar")) { + i++; + if (i < args.length) { + if (currentJar != null) { + System.err.println("--current-jar should only be specified once"); + error = true; + } + String path = args[i]; + currentJar = new File(path); + } else { + System.err.println("Missing argument after " + arg); + error = true; + } + } else if (arg.equals("--min-api")) { + i++; + if (i < args.length) { + minApi = Integer.parseInt(args[i]); + } else { + System.err.println("Missing number >= 1 after " + arg); + error = true; + } + } else if (arg.length() >= 2 && arg.substring(0, 2).equals("--")) { + System.err.println("Unknown argument: " + arg); + error = true; + + } else if (outPath == null) { + outPath = arg; + + } else if (new File(arg).isDirectory()) { + String pattern = arg; + if (!pattern.endsWith(File.separator)) { + pattern += File.separator; + } + pattern += "platforms" + File.separator + "android-%" + File.separator + "android.jar"; + patterns.add(pattern); + + } else { + System.err.println("Unknown argument: " + arg); + error = true; + } + } + + if (!error && outPath == null) { + System.err.println("Missing out file path"); + error = true; + } + + if (!error && patterns.isEmpty()) { + System.err.println("Missing SdkFolder or --pattern."); + error = true; + } + + if (currentJar != null && currentApi == -1 || currentJar == null && currentApi != -1) { + System.err.println("You must specify both --current-jar and --current-version (or neither one)"); + error = true; + } + + // The SDK version number + if (currentCodename != null && !"REL".equals(currentCodename)) { + currentApi++; + } + + if (error) { + printUsage(); + System.exit(1); + } + + try { + if (!generate(minApi, currentApi, currentJar, patterns, outPath)) { + System.exit(1); + } + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); + } + } + + private static boolean generate(int minApi, + int currentApi, + @NonNull File currentJar, + @NonNull List<String> patterns, + @NonNull String outPath) throws IOException { + AndroidJarReader reader = new AndroidJarReader(patterns, minApi, currentJar, currentApi); + Api api = reader.getApi(); + return createApiFile(new File(outPath), api); + } + + public static boolean generate(@NonNull File[] apiLevels, @NonNull File outputFile) throws IOException { + AndroidJarReader reader = new AndroidJarReader(apiLevels); + Api api = reader.getApi(); + return createApiFile(outputFile, api); + } + + private static void printUsage() { + System.err.println("\nGenerates a single API file from the content of an SDK."); + System.err.println("Usage:"); + System.err.println("\tApiCheck [--min-api=1] OutFile [SdkFolder | --pattern sdk/%/android.jar]+"); + System.err.println("Options:"); + System.err.println("--min-api <int> : The first API level to consider (>=1)."); + System.err.println("--pattern <pattern>: Path pattern to find per-API android.jar files, where\n" + + " '%' is replaced by the API level."); + System.err.println("--current-jar <path>: Path pattern to find the current android.jar"); + System.err.println("--current-version <int>: The API level for the current API"); + System.err.println("--current-codename <name>: REL, if a release, or codename for previews"); + System.err.println("SdkFolder: if given, this adds the pattern\n" + + " '$SdkFolder/platforms/android-%/android.jar'"); + System.err.println("If multiple --pattern are specified, they are tried in the order given.\n"); + } + + /** + * Creates the simplified diff-based API level. + * + * @param outFile the output file + * @param api the api to write + */ + private static boolean createApiFile(File outFile, Api api) { + PrintStream stream = null; + try { + File parentFile = outFile.getParentFile(); + if (!parentFile.exists()) { + boolean ok = parentFile.mkdirs(); + if (!ok) { + System.err.println("Could not create directory " + parentFile); + return false; + } + } + stream = new PrintStream(outFile, "UTF-8"); + stream.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); + api.print(stream); + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + if (stream != null) { + stream.close(); + } + } + + return true; + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java new file mode 100644 index 0000000..dbec782 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java @@ -0,0 +1,877 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * 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.doclava1; + +import com.android.tools.lint.annotations.Extractor; +import com.android.tools.lint.checks.infrastructure.ClassNameKt; +import com.android.tools.metalava.model.AnnotationItem; +import com.android.tools.metalava.model.text.TextClassItem; +import com.android.tools.metalava.model.text.TextConstructorItem; +import com.android.tools.metalava.model.text.TextFieldItem; +import com.android.tools.metalava.model.text.TextMethodItem; +import com.android.tools.metalava.model.text.TextPackageItem; +import com.android.tools.metalava.model.text.TextParameterItem; +import com.android.tools.metalava.model.text.TextTypeItem; +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import kotlin.Pair; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.android.tools.metalava.model.FieldItemKt.javaUnescapeString; + +// +// Copied from doclava1, but adapted to metalava's code model (plus tweaks to handle +// metalava's richer files, e.g. annotations) +// +public class ApiFile { + public static ApiInfo parseApi(File file, + boolean kotlinStyleNulls, + boolean supportsStagedNullability) throws ApiParseException { + try { + String apiText = Files.asCharSource(file, Charsets.UTF_8).read(); + return parseApi(file.getPath(), apiText, kotlinStyleNulls, supportsStagedNullability); + } catch (IOException ex) { + throw new ApiParseException("Error reading API file", ex); + } + } + + public static ApiInfo parseApi(String filename, String apiText, + boolean kotlinStyleNulls, + boolean supportsStagedNullability) throws ApiParseException { + if (apiText.contains("/*")) { + apiText = ClassNameKt.stripComments(apiText, false); // line comments are used to stash field constants + } + + final Tokenizer tokenizer = new Tokenizer(filename, apiText.toCharArray()); + final ApiInfo api = new ApiInfo(); + api.setSupportsStagedNullability(supportsStagedNullability); + api.setKotlinStyleNulls(kotlinStyleNulls); + + while (true) { + String token = tokenizer.getToken(); + if (token == null) { + break; + } + if ("package".equals(token)) { + parsePackage(api, tokenizer); + } else { + throw new ApiParseException("expected package got " + token, tokenizer.getLine()); + } + } + + api.postProcess(); + + return api; + } + + private static void parsePackage(ApiInfo api, Tokenizer tokenizer) + throws ApiParseException { + String token; + String name; + TextPackageItem pkg; + + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + name = token; + pkg = new TextPackageItem(api, name, tokenizer.pos()); + token = tokenizer.requireToken(); + if (!"{".equals(token)) { + throw new ApiParseException("expected '{' got " + token, tokenizer.getLine()); + } + while (true) { + token = tokenizer.requireToken(); + if ("}".equals(token)) { + break; + } else { + parseClass(api, pkg, tokenizer, token); + } + } + api.addPackage(pkg); + } + + private static void parseClass(ApiInfo api, TextPackageItem pkg, Tokenizer tokenizer, String token) + throws ApiParseException { + boolean pub = false; + boolean prot = false; + boolean priv = false; + boolean stat = false; + boolean fin = false; + boolean abs = false; + boolean dep = false; + boolean isInterface = false; + boolean isAnnotation = false; + boolean isEnum = false; + String name; + String qname; + String ext = null; + TextClassItem cl; + + // Metalava: including annotations in file now + List<String> annotations = null; + Pair<String, List<String>> result = getAnnotations(tokenizer, token); + if (result != null) { + token = result.component1(); + annotations = result.component2(); + } + + if ("public".equals(token)) { + pub = true; + token = tokenizer.requireToken(); + } else if ("protected".equals(token)) { + prot = true; + token = tokenizer.requireToken(); + } else if ("private".equals(token)) { + priv = true; + token = tokenizer.requireToken(); + } + if ("static".equals(token)) { + stat = true; + token = tokenizer.requireToken(); + } + if ("final".equals(token)) { + fin = true; + token = tokenizer.requireToken(); + } + if ("abstract".equals(token)) { + abs = true; + token = tokenizer.requireToken(); + } + if ("deprecated".equals(token)) { + dep = true; + token = tokenizer.requireToken(); + } + if ("class".equals(token)) { + token = tokenizer.requireToken(); + } else if ("interface".equals(token)) { + isInterface = true; + token = tokenizer.requireToken(); + } else if ("@interface".equals(token)) { + // Annotation + abs = true; + isAnnotation = true; + token = tokenizer.requireToken(); + } else if ("enum".equals(token)) { + isEnum = true; + fin = true; + ext = "java.lang.Enum"; + token = tokenizer.requireToken(); + } else { + throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine()); + } + assertIdent(tokenizer, token); + name = token; + qname = qualifiedName(pkg.name(), name); + final TextTypeItem typeInfo = api.obtainTypeFromString(qname); + // Simple type info excludes the package name (but includes enclosing class names) + final TextTypeItem simpleTypeInfo = api.obtainTypeFromString(name); + token = tokenizer.requireToken(); + + cl = new TextClassItem(api, tokenizer.pos(), pub, prot, + priv, stat, isInterface, abs, isEnum, isAnnotation, + fin, typeInfo.toErasedTypeString(), typeInfo.qualifiedTypeName(), + simpleTypeInfo.toErasedTypeString(), annotations); + cl.setContainingPackage(pkg); + cl.setTypeInfo(typeInfo); + cl.setDeprecated(dep); + if ("extends".equals(token)) { + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + ext = token; + token = tokenizer.requireToken(); + } + // Resolve superclass after done parsing + api.mapClassToSuper(cl, ext); + if ("implements".equals(token)) { + while (true) { + token = tokenizer.requireToken(); + if ("{".equals(token)) { + break; + } else { + /// TODO + if (!",".equals(token)) { + api.mapClassToInterface(cl, token); + } + } + } + } + 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")) { + cl.setIsAnnotationType(true); + } + if (!"{".equals(token)) { + throw new ApiParseException("expected {", tokenizer.getLine()); + } + token = tokenizer.requireToken(); + while (true) { + if ("}".equals(token)) { + break; + } else if ("ctor".equals(token)) { + token = tokenizer.requireToken(); + parseConstructor(api, tokenizer, cl, token); + } else if ("method".equals(token)) { + token = tokenizer.requireToken(); + parseMethod(api, tokenizer, cl, token); + } else if ("field".equals(token)) { + token = tokenizer.requireToken(); + parseField(api, tokenizer, cl, token, false); + } else if ("enum_constant".equals(token)) { + token = tokenizer.requireToken(); + parseField(api, tokenizer, cl, token, true); + } else { + throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer.getLine()); + } + token = tokenizer.requireToken(); + } + pkg.addClass(cl); + } + + private static Pair<String, List<String>> processKotlinTypeSuffix(ApiInfo api, String token, List<String> annotations) throws ApiParseException { + if (api.getKotlinStyleNulls()) { + if (token.endsWith("?")) { + token = token.substring(0, token.length() - 1); + annotations = mergeAnnotations(annotations, Extractor.SUPPORT_NULLABLE); + } else if (token.endsWith("!")) { + token = token.substring(0, token.length() - 1); + } else if (!token.endsWith("!")) { + if (!TextTypeItem.Companion.isPrimitive(token)) { // Don't add nullness on primitive types like void + annotations = mergeAnnotations(annotations, Extractor.SUPPORT_NOTNULL); + } + } + } else if (token.endsWith("?") || token.endsWith("!")) { + throw new ApiParseException("Did you forget to supply --input-kotlin-nulls? Found Kotlin-style null type suffix when parser was not configured " + + "to interpret signature file that way: " + token); + } + //noinspection unchecked + return new Pair<>(token, annotations); + } + + private static Pair<String, List<String>> getAnnotations(Tokenizer tokenizer, String token) throws ApiParseException { + List<String> annotations = null; + + while (true) { + if (token.startsWith("@")) { + // Annotation + String annotation = token; + if (annotation.indexOf('.') == -1) { + // Restore annotations that were shortened on export + annotation = AnnotationItem.Companion.unshortenAnnotation(annotation); + } + token = tokenizer.requireToken(); + if (token.equals("(")) { + // Annotation arguments + int start = tokenizer.offset() - 1; + while (!token.equals(")")) { + token = tokenizer.requireToken(); + } + annotation += tokenizer.getStringFromOffset(start); + token = tokenizer.requireToken(); + } + if (annotations == null) { + annotations = new ArrayList<>(); + } + annotations.add(annotation); + } else { + break; + } + } + + if (annotations != null) { + //noinspection unchecked + return new Pair<>(token, annotations); + } else { + return null; + } + } + + private static void parseConstructor(ApiInfo api, Tokenizer tokenizer, TextClassItem cl, String token) + throws ApiParseException { + boolean pub = false; + boolean prot = false; + boolean priv = false; + boolean dep = false; + String name; + TextConstructorItem method; + + // Metalava: including annotations in file now + List<String> annotations = null; + Pair<String, List<String>> result = getAnnotations(tokenizer, token); + if (result != null) { + token = result.component1(); + annotations = result.component2(); + } + + if ("public".equals(token)) { + pub = true; + token = tokenizer.requireToken(); + } else if ("protected".equals(token)) { + prot = true; + token = tokenizer.requireToken(); + } else if ("private".equals(token)) { + priv = true; + token = tokenizer.requireToken(); + } + if ("deprecated".equals(token)) { + dep = true; + token = tokenizer.requireToken(); + } + assertIdent(tokenizer, token); + name = token.substring(token.lastIndexOf('.') + 1); // For inner classes, strip outer classes from name + token = tokenizer.requireToken(); + if (!"(".equals(token)) { + throw new ApiParseException("expected (", tokenizer.getLine()); + } + method = new TextConstructorItem(api, /*typeParameters*/ + name, /*signature*/ cl, pub, prot, priv, false/*isFinal*/, + false/*isStatic*/, /*isSynthetic*/ false/*isAbstract*/, false/*isSynthetic*/, + false/*isNative*/, false/* isDefault */, + /*isAnnotationElement*/ /*flatSignature*/ + /*overriddenMethod*/ cl.asTypeInfo(), + /*thrownExceptions*/ tokenizer.pos(), annotations); + method.setDeprecated(dep); + token = tokenizer.requireToken(); + parseParameterList(api, tokenizer, method, /*new HashSet<String>(),*/ token); + token = tokenizer.requireToken(); + if ("throws".equals(token)) { + token = parseThrows(tokenizer, method); + } + if (!";".equals(token)) { + throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); + } + cl.addConstructor(method); + } + + private static void parseMethod(ApiInfo api, Tokenizer tokenizer, TextClassItem cl, String token) + throws ApiParseException { + boolean pub = false; + boolean prot = false; + boolean priv = false; + boolean stat = false; + boolean fin = false; + boolean abs = false; + boolean dep = false; + boolean syn = false; + boolean def = false; + TextTypeItem returnType; + String name; + TextMethodItem method; + String typeParameterList = null; + + // Metalava: including annotations in file now + List<String> annotations = null; + Pair<String, List<String>> result = getAnnotations(tokenizer, token); + if (result != null) { + token = result.component1(); + annotations = result.component2(); + } + + if ("public".equals(token)) { + pub = true; + token = tokenizer.requireToken(); + } else if ("protected".equals(token)) { + prot = true; + token = tokenizer.requireToken(); + } else if ("private".equals(token)) { + priv = true; + token = tokenizer.requireToken(); + } + if ("default".equals(token)) { + def = true; + token = tokenizer.requireToken(); + } + if ("static".equals(token)) { + stat = true; + token = tokenizer.requireToken(); + } + if ("final".equals(token)) { + fin = true; + token = tokenizer.requireToken(); + } + if ("abstract".equals(token)) { + abs = true; + token = tokenizer.requireToken(); + } + if ("deprecated".equals(token)) { + dep = true; + token = tokenizer.requireToken(); + } + if ("synchronized".equals(token)) { + syn = true; + token = tokenizer.requireToken(); + } + if ("<".equals(token)) { + typeParameterList = parseTypeParameterList(tokenizer); + token = tokenizer.requireToken(); + } + assertIdent(tokenizer, token); + + Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); + token = kotlinTypeSuffix.getFirst(); + annotations = kotlinTypeSuffix.getSecond(); + returnType = api.obtainTypeFromString(token); + + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + name = token; + method = new TextMethodItem( + api, name, /*signature*/ cl, + pub, prot, priv, fin, stat, abs/*isAbstract*/, + syn, false/*isNative*/, def/*isDefault*/, + returnType, tokenizer.pos(), annotations); + method.setDeprecated(dep); + method.setTypeParameterList(typeParameterList); + token = tokenizer.requireToken(); + if (!"(".equals(token)) { + throw new ApiParseException("expected (", tokenizer.getLine()); + } + token = tokenizer.requireToken(); + parseParameterList(api, tokenizer, method, /*typeVariableNames,*/ token); + token = tokenizer.requireToken(); + if ("throws".equals(token)) { + token = parseThrows(tokenizer, method); + } + if (!";".equals(token)) { + throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); + } + cl.addMethod(method); + } + + private static List<String> mergeAnnotations(List<String> annotations, String annotation) { + if (annotations == null) { + annotations = new ArrayList<>(); + } + annotations.add("@" + annotation); + return annotations; + } + + private static void parseField(ApiInfo api, Tokenizer tokenizer, TextClassItem cl, String token, boolean isEnum) + throws ApiParseException { + boolean pub = false; + boolean prot = false; + boolean priv = false; + boolean stat = false; + boolean fin = false; + boolean dep = false; + boolean trans = false; + boolean vol = false; + String type; + String name; + String val = null; + Object v; + TextFieldItem field; + + // Metalava: including annotations in file now + List<String> annotations = null; + Pair<String, List<String>> result = getAnnotations(tokenizer, token); + if (result != null) { + token = result.component1(); + annotations = result.component2(); + } + + if ("public".equals(token)) { + pub = true; + token = tokenizer.requireToken(); + } else if ("protected".equals(token)) { + prot = true; + token = tokenizer.requireToken(); + } else if ("private".equals(token)) { + priv = true; + token = tokenizer.requireToken(); + } + if ("static".equals(token)) { + stat = true; + token = tokenizer.requireToken(); + } + if ("final".equals(token)) { + fin = true; + token = tokenizer.requireToken(); + } + if ("deprecated".equals(token)) { + dep = true; + token = tokenizer.requireToken(); + } + if ("transient".equals(token)) { + trans = true; + token = tokenizer.requireToken(); + } + if ("volatile".equals(token)) { + vol = true; + token = tokenizer.requireToken(); + } + assertIdent(tokenizer, token); + + Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); + token = kotlinTypeSuffix.getFirst(); + annotations = kotlinTypeSuffix.getSecond(); + type = token; + TextTypeItem typeInfo = api.obtainTypeFromString(type); + + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + name = token; + token = tokenizer.requireToken(); + if ("=".equals(token)) { + token = tokenizer.requireToken(false); + val = token; + token = tokenizer.requireToken(); + } + if (!";".equals(token)) { + throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); + } + try { + v = parseValue(type, val); + } catch (ApiParseException ex) { + ex.line = tokenizer.getLine(); + throw ex; + } + + field = new TextFieldItem(api, name, cl, pub, prot, priv, fin, stat, + trans, vol, typeInfo, v, tokenizer.pos(), + annotations); + field.setDeprecated(dep); + if (isEnum) { + cl.addEnumConstant(field); + } else { + cl.addField(field); + } + } + + public static Object parseValue(String type, String val) throws ApiParseException { + if (val != null) { + if ("boolean".equals(type)) { + return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE; + } else if ("byte".equals(type)) { + return Integer.valueOf(val); + } else if ("short".equals(type)) { + return Integer.valueOf(val); + } else if ("int".equals(type)) { + return Integer.valueOf(val); + } else if ("long".equals(type)) { + return Long.valueOf(val.substring(0, val.length() - 1)); + } else if ("float".equals(type)) { + if ("(1.0f/0.0f)".equals(val) || "(1.0f / 0.0f)".equals(val)) { + return Float.POSITIVE_INFINITY; + } else if ("(-1.0f/0.0f)".equals(val) || "(-1.0f / 0.0f)".equals(val)) { + return Float.NEGATIVE_INFINITY; + } else if ("(0.0f/0.0f)".equals(val) || "(0.0f / 0.0f)".equals(val)) { + return Float.NaN; + } else { + return Float.valueOf(val); + } + } else if ("double".equals(type)) { + if ("(1.0/0.0)".equals(val) || "(1.0 / 0.0)".equals(val)) { + return Double.POSITIVE_INFINITY; + } else if ("(-1.0/0.0)".equals(val) || "(-1.0 / 0.0)".equals(val)) { + return Double.NEGATIVE_INFINITY; + } else if ("(0.0/0.0)".equals(val) || "(0.0 / 0.0)".equals(val)) { + return Double.NaN; + } else { + return Double.valueOf(val); + } + } else if ("char".equals(type)) { + return (char) Integer.parseInt(val); + } else if ("java.lang.String".equals(type)) { + if ("null".equals(val)) { + return null; + } else { + return javaUnescapeString(val.substring(1, val.length() - 1)); + } + } + } + if ("null".equals(val)) { + return null; + } else { + return val; + } + } + + private static String parseTypeParameterList(Tokenizer tokenizer) throws ApiParseException { + String token; + + int start = tokenizer.offset() - 1; + int balance = 1; + while (balance > 0) { + token = tokenizer.requireToken(); + if (token.equals("<")) { + balance++; + } else if (token.equals(">")) { + balance--; + } + } + + return tokenizer.getStringFromOffset(start); + } + + private static void parseParameterList(ApiInfo api, Tokenizer tokenizer, TextMethodItem method, + String token) throws ApiParseException { + int index = 0; + while (true) { + if (")".equals(token)) { + return; + } + + // Metalava: including annotations in file now + List<String> annotations = null; + Pair<String, List<String>> result = getAnnotations(tokenizer, token); + if (result != null) { + token = result.component1(); + annotations = result.component2(); + } + + Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); + token = kotlinTypeSuffix.getFirst(); + annotations = kotlinTypeSuffix.getSecond(); + String type = token; + TextTypeItem typeInfo = api.obtainTypeFromString(token); + + String name = null; + token = tokenizer.requireToken(); + String publicName; + if (isIdent(token)) { + name = token; + publicName = name; + token = tokenizer.requireToken(); + } else { + name = "arg" + (index + 1); + publicName = null; + } + if (",".equals(token)) { + token = tokenizer.requireToken(); + } else if (")".equals(token)) { + } else { + throw new ApiParseException("expected , found " + token, tokenizer.getLine()); + } + + method.addParameter(new TextParameterItem(api, method, name, publicName, index, type, + typeInfo, + type.endsWith("..."), + tokenizer.pos(), + annotations)); + if (type.endsWith("...")) { + method.setVarargs(true); + } + index++; + } + } + + private static String parseThrows(Tokenizer tokenizer, TextMethodItem method) + throws ApiParseException { + String token = tokenizer.requireToken(); + boolean comma = true; + while (true) { + if (";".equals(token)) { + return token; + } else if (",".equals(token)) { + if (comma) { + throw new ApiParseException("Expected exception, got ','", tokenizer.getLine()); + } + comma = true; + } else { + if (!comma) { + throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer.getLine()); + } + comma = false; + method.addException(token); + } + token = tokenizer.requireToken(); + } + } + + private static String qualifiedName(String pkg, String className) { + return pkg + "." + className; + } + + private static boolean isIdent(String token) { + return isident(token.charAt(0)); + } + + private static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException { + if (!isident(token.charAt(0))) { + throw new ApiParseException("Expected identifier: " + token, tokenizer.getLine()); + } + } + + static class Tokenizer { + char[] mBuf; + String mFilename; + int mPos; + int mLine = 1; + + Tokenizer(String filename, char[] buf) { + mFilename = filename; + mBuf = buf; + } + + public SourcePositionInfo pos() { + return new SourcePositionInfo(mFilename, mLine, 0); + } + + public int getLine() { + return mLine; + } + + boolean eatWhitespace() { + boolean ate = false; + while (mPos < mBuf.length && isspace(mBuf[mPos])) { + if (mBuf[mPos] == '\n') { + mLine++; + } + mPos++; + ate = true; + } + return ate; + } + + boolean eatComment() { + if (mPos + 1 < mBuf.length) { + if (mBuf[mPos] == '/' && mBuf[mPos + 1] == '/') { + mPos += 2; + while (mPos < mBuf.length && !isnewline(mBuf[mPos])) { + mPos++; + } + return true; + } + } + return false; + } + + void eatWhitespaceAndComments() { + while (eatWhitespace() || eatComment()) { + } + } + + public String requireToken() throws ApiParseException { + return requireToken(true); + } + + public String requireToken(boolean parenIsSep) throws ApiParseException { + final String token = getToken(parenIsSep); + if (token != null) { + return token; + } else { + throw new ApiParseException("Unexpected end of file", mLine); + } + } + + public String getToken() throws ApiParseException { + return getToken(true); + } + + public int offset() { + return mPos; + } + + public String getStringFromOffset(int offset) { + return new String(mBuf, offset, mPos - offset); + } + + public String getToken(boolean parenIsSep) throws ApiParseException { + eatWhitespaceAndComments(); + if (mPos >= mBuf.length) { + return null; + } + final int line = mLine; + final char c = mBuf[mPos]; + final int start = mPos; + mPos++; + if (c == '"') { + final int STATE_BEGIN = 0; + final int STATE_ESCAPE = 1; + int state = STATE_BEGIN; + while (true) { + if (mPos >= mBuf.length) { + throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); + } + final char k = mBuf[mPos]; + if (k == '\n' || k == '\r') { + throw new ApiParseException("Unexpected newline for \" starting at " + line, mLine); + } + mPos++; + switch (state) { + case STATE_BEGIN: + switch (k) { + case '\\': + state = STATE_ESCAPE; + mPos++; + break; + case '"': + return new String(mBuf, start, mPos - start); + } + case STATE_ESCAPE: + state = STATE_BEGIN; + break; + } + } + } else if (issep(c, parenIsSep)) { + return "" + c; + } else { + int genericDepth = 0; + do { + while (mPos < mBuf.length && !isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) { + mPos++; + } + if (mPos < mBuf.length) { + if (mBuf[mPos] == '<') { + genericDepth++; + mPos++; + } else if (genericDepth != 0) { + if (mBuf[mPos] == '>') { + genericDepth--; + } + mPos++; + } + } + } while (mPos < mBuf.length + && ((!isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) || genericDepth != 0)); + if (mPos >= mBuf.length) { + throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); + } + return new String(mBuf, start, mPos - start); + } + } + } + + static boolean isspace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + static boolean isnewline(char c) { + return c == '\n' || c == '\r'; + } + + static boolean issep(char c, boolean parenIsSep) { + if (parenIsSep) { + if (c == '(' || c == ')') { + return true; + } + } + return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>'; + } + + private static boolean isident(char c) { + if (c == '"' || issep(c, true)) { + return false; + } + return true; + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt b/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt new file mode 100644 index 0000000..1993aa5 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.doclava1 + +import com.android.annotations.NonNull +import com.android.tools.metalava.CodebaseComparator +import com.android.tools.metalava.ComparisonVisitor +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultCodebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.PackageList +import com.android.tools.metalava.model.text.TextBackedAnnotationItem +import com.android.tools.metalava.model.text.TextClassItem +import com.android.tools.metalava.model.text.TextMethodItem +import com.android.tools.metalava.model.text.TextPackageItem +import com.android.tools.metalava.model.text.TextTypeItem +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import java.util.ArrayList +import java.util.HashMap +import java.util.function.Predicate + +// Copy of ApiInfo in doclava1 (converted to Kotlin + some cleanup to make it work with metalava's data structures. +// (Converted to Kotlin such that I can inherit behavior via interfaces, in particular Codebase.) +class ApiInfo : DefaultCodebase() { + /** + * Whether types should be interpreted to be in Kotlin format (e.g. ? suffix means nullable, + * ! suffix means unknown, and absence of a suffix means not nullable. + */ + var kotlinStyleNulls = false + + private val mPackages = HashMap<String, TextPackageItem>(300) + private val mAllClasses = HashMap<String, TextClassItem>(30000) + private val mClassToSuper = HashMap<TextClassItem, String>(30000) + private val mClassToInterface = HashMap<TextClassItem, ArrayList<String>>(10000) + + override var description = "Codebase" + + override fun trustedApi(): Boolean = true + + override fun getPackages(): PackageList { + val list = ArrayList<PackageItem>(mPackages.values) + list.sortWith(PackageItem.comparator) + return PackageList(list) + } + + override fun size(): Int { + return mPackages.size + } + + override fun findClass(@NonNull className: String): TextClassItem? { + return mAllClasses[className] + } + + private fun resolveInterfaces() { + for (cl in mAllClasses.values) { + val ifaces = mClassToInterface[cl] ?: continue + for (iface in ifaces) { + var ci: TextClassItem? = mAllClasses[iface] + if (ci == null) { + // Interface not provided by this codebase. Inject a stub. + ci = TextClassItem.createInterfaceStub(this, iface) + } + cl.addInterface(ci) + } + } + } + + override fun supportsDocumentation(): Boolean = false + + fun mapClassToSuper(classInfo: TextClassItem, superclass: String?) { + superclass?.let { mClassToSuper.put(classInfo, superclass) } + } + + fun mapClassToInterface(classInfo: TextClassItem, iface: String) { + if (!mClassToInterface.containsKey(classInfo)) { + mClassToInterface.put(classInfo, ArrayList()) + } + mClassToInterface[classInfo]?.add(iface) + } + + fun implementsInterface(classInfo: TextClassItem, iface: String): Boolean { + return mClassToInterface[classInfo]?.contains(iface) ?: false + } + + fun addPackage(pInfo: TextPackageItem) { + // track the set of organized packages in the API + mPackages.put(pInfo.name(), pInfo) + + // accumulate a direct map of all the classes in the API + for (cl in pInfo.allClasses()) { + mAllClasses.put(cl.qualifiedName(), cl as TextClassItem) + } + } + + private fun resolveSuperclasses() { + for (cl in mAllClasses.values) { + // java.lang.Object has no superclass + if (cl.isJavaLangObject()) { + continue + } + var scName: String? = mClassToSuper[cl] + if (scName == null) { + scName = "java.lang.Object" + } + var superclass: TextClassItem? = mAllClasses[scName] + if (superclass == null) { + // Superclass not provided by this codebase. Inject a stub. + superclass = TextClassItem.createClassStub(this, scName) + } + cl.setSuperClass(superclass) + } + } + + private fun resolveThrowsClasses() { + for (cl in mAllClasses.values) { + for (methodItem in cl.methods()) { + val methodInfo = methodItem as TextMethodItem + val names = methodInfo.throwsTypeNames() + if (!names.isEmpty()) { + val result = ArrayList<TextClassItem>() + for (exception in names) { + var exceptionClass: TextClassItem? = mAllClasses[exception] + if (exceptionClass == null) { + // Exception not provided by this codebase. Inject a stub. + exceptionClass = TextClassItem.createClassStub( + this, exception + ) + } + result.add(exceptionClass) + } + methodInfo.setThrowsList(result) + } + } + + // java.lang.Object has no superclass + var scName: String? = mClassToSuper[cl] + if (scName == null) { + scName = "java.lang.Object" + } + var superclass: TextClassItem? = mAllClasses[scName] + if (superclass == null) { + // Superclass not provided by this codebase. Inject a stub. + superclass = TextClassItem.createClassStub(this, scName) + } + cl.setSuperClass(superclass) + } + } + + private fun resolveInnerClasses() { + mPackages.values + .asSequence() + .map { it.classList().listIterator() as MutableListIterator<ClassItem> } + .forEach { + while (it.hasNext()) { + val cl = it.next() as TextClassItem + val name = cl.name + var index = name.lastIndexOf('.') + if (index != -1) { + cl.name = name.substring(index + 1) + val qualifiedName = cl.qualifiedName + index = qualifiedName.lastIndexOf('.') + assert(index != -1) { qualifiedName } + val outerClassName = qualifiedName.substring(0, index) + val outerClass = mAllClasses[outerClassName]!! + cl.containingClass = outerClass + outerClass.addInnerClass(cl) + + // Should no longer be listed as top level + it.remove() + } + } + } + } + + fun postProcess() { + resolveSuperclasses() + resolveInterfaces() + resolveThrowsClasses() + resolveInnerClasses() + } + + override fun findPackage(pkgName: String): PackageItem? { + return mPackages.values.firstOrNull { pkgName == it.qualifiedName() } + } + + override fun accept(visitor: ItemVisitor) { + getPackages().accept(visitor) + } + + override fun acceptTypes(visitor: TypeVisitor) { + getPackages().acceptTypes(visitor) + } + + override fun compareWith(visitor: ComparisonVisitor, other: Codebase, filter: Predicate<Item>?) { + CodebaseComparator().compare(visitor, this, other, filter) + } + + override fun createAnnotation(source: String, context: Item?, mapName: Boolean): AnnotationItem { + return TextBackedAnnotationItem(this, source, mapName) + } + + override fun toString(): String { + return description + } + + override fun unsupported(desc: String?): Nothing { + error(desc ?: "Not supported for a signature-file based codebase") + } + + override fun filter(filterEmit: Predicate<Item>, filterReference: Predicate<Item>): Codebase { + unsupported() + } + + // Copied from Converter: + + fun obtainTypeFromString(type: String): TextTypeItem { + return mTypesFromString.obtain(type) as TextTypeItem + } + + private val mTypesFromString = object : Cache(this) { + override fun make(o: Any): Any { + val name = o as String + + return TextTypeItem(codebase, name) + } + } + + private abstract class Cache(val codebase: ApiInfo) { + + protected var mCache = HashMap<Any, Any>() + + internal fun obtain(o: Any?): Any? { + if (o == null) { + return null + } + var r: Any? = mCache[o] + if (r == null) { + r = make(o) + mCache.put(o, r) + } + return r + } + + protected abstract fun make(o: Any): Any + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiParseException.java b/src/main/java/com/android/tools/metalava/doclava1/ApiParseException.java new file mode 100644 index 0000000..6711f8c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiParseException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.doclava1; + +// +// Copied from doclava1, but adapted to metalava's code model +// +public final class ApiParseException extends Exception { + public String file; + public int line; + + public ApiParseException(String message) { + super(message); + } + + public ApiParseException(String message, Exception cause) { + super(message, cause); + if (cause instanceof ApiParseException) { + this.line = ((ApiParseException) cause).line; + } + } + + public ApiParseException(String message, int line) { + super(message); + this.line = line; + } + + public String getMessage() { + if (line > 0) { + return super.getMessage() + " line " + line; + } else { + return super.getMessage(); + } + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt b/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt new file mode 100644 index 0000000..4b5af4c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt @@ -0,0 +1,103 @@ +package com.android.tools.metalava.doclava1 + +import com.android.tools.metalava.Options +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MemberItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.options +import java.util.function.Predicate + +// Ported from doclava1 + +/** + * Predicate that decides if the given member should be considered part of an + * API surface area. To make the most accurate decision, it searches for + * signals on the member, all containing classes, and all containing packages. + */ +class ApiPredicate( + val codebase: Codebase, + /** + * Set if the value of [MemberItem.hasShowAnnotation] should be + * ignored. That is, this predicate will assume that all encountered members + * match the "shown" requirement. + * + * This is typically useful when generating "current.txt", when no + * [Options.showAnnotations] have been defined. + */ + private val ignoreShown: Boolean = options.showUnannotated, + + /** + * Set if the value of [MemberItem.removed] should be ignored. + * That is, this predicate will assume that all encountered members match + * the "removed" requirement. + * + * This is typically useful when generating "removed.txt", when it's okay to + * reference both current and removed APIs. + */ + private val ignoreRemoved: Boolean = false, + + /** + * Set what the value of [MemberItem.removed] must be equal to in + * order for a member to match. + * + * This is typically useful when generating "removed.txt", when you only + * want to match members that have actually been removed. + */ + private val matchRemoved: Boolean = false, + + /** Whether we allow matching items loaded from jar files instead of sources */ + private val allowFromJar: Boolean = true +) : Predicate<Item> { + + override fun test(member: Item): Boolean { + if (!allowFromJar && member.isFromClassPath()) { + return false + } + + var visible = member.isPublic || member.isProtected + var hidden = member.hidden + if (!visible || hidden) { + return false + } + + var hasShowAnnotation = ignoreShown || member.hasShowAnnotation() + var docOnly = member.docOnly + var removed = member.removed + + var clazz: ClassItem? = when (member) { + is MemberItem -> member.containingClass() + is ClassItem -> member + else -> null + } + + if (clazz != null) { + var pkg: PackageItem? = clazz.containingPackage() + while (pkg != null) { + hidden = hidden or pkg.hidden + docOnly = docOnly or pkg.docOnly + removed = removed or pkg.removed + pkg = pkg.containingPackage() + } + } + while (clazz != null) { + visible = visible and (clazz.isPublic || clazz.isProtected) + hasShowAnnotation = hasShowAnnotation or (ignoreShown || clazz.hasShowAnnotation()) + hidden = hidden or clazz.hidden + docOnly = docOnly or clazz.docOnly + removed = removed or clazz.removed + clazz = clazz.containingClass() + } + + if (ignoreRemoved) { + removed = matchRemoved + } + + if (docOnly && options.includeDocOnly) { + docOnly = false + } + + return visible && hasShowAnnotation && !hidden && !docOnly && removed == matchRemoved + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/ElidingPredicate.kt b/src/main/java/com/android/tools/metalava/doclava1/ElidingPredicate.kt new file mode 100644 index 0000000..3f7c9d7 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ElidingPredicate.kt @@ -0,0 +1,31 @@ +package com.android.tools.metalava.doclava1 + +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem + +import java.util.function.Predicate + +// Ported from doclava1 + +/** + * Filter that will elide exact duplicate methods that are already included + * in another superclass/interfaces. + */ +class ElidingPredicate(private val wrapped: Predicate<Item>) : Predicate<Item> { + + override fun test(method: Item): Boolean { + // This method should be included, but if it's an exact duplicate + // override then we can elide it. + return if (method is MethodItem && !method.isConstructor()) { + val differentSuper = method.findPredicateSuperMethod(Predicate { test -> + // We're looking for included and perfect signature + wrapped.test(test) && + test is MethodItem && + MethodItem.sameSignature(method, test, false) + }) + differentSuper == null + } else { + true + } + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/Errors.java b/src/main/java/com/android/tools/metalava/doclava1/Errors.java new file mode 100644 index 0000000..b346272 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2017 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.doclava1; + +import com.android.annotations.Nullable; +import com.android.tools.metalava.Severity; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static com.android.sdklib.SdkVersionInfo.underlinesToCamelCase; +import static com.android.tools.metalava.Severity.ERROR; +import static com.android.tools.metalava.Severity.HIDDEN; +import static com.android.tools.metalava.Severity.INHERIT; +import static com.android.tools.metalava.Severity.LINT; +import static com.android.tools.metalava.Severity.WARNING; + +// Copied from doclava1 (and a bunch of stuff left alone; preserving to have same error id's) +public class Errors { + public static class Error { + public final int code; + + private Severity level; + private Severity defaultLevel; + + /** + * The name of this error if known + */ + @Nullable + public String name; + + /** + * When {@code level} is set to {@link Severity#INHERIT}, this is the parent from + * which the error will inherit its level. + */ + private final Error parent; + + public Error(int code, Severity level) { + this.code = code; + this.level = level; + this.defaultLevel = level; + this.parent = null; + sErrors.add(this); + } + + public Error(int code, Error parent) { + this.code = code; + this.level = Severity.INHERIT; + this.defaultLevel = Severity.INHERIT; + this.parent = parent; + sErrors.add(this); + } + + /** + * Returns the implied level for this error. + * <p> + * If the level is {@link Severity#INHERIT}, the level will be returned for the + * parent. + * + * @throws IllegalStateException if the level is {@link Severity#INHERIT} and the + * parent is {@code null} + */ + public Severity getLevel() { + if (level == INHERIT) { + if (parent == null) { + throw new IllegalStateException("Error with level INHERIT must have non-null parent"); + } + return parent.getLevel(); + } + return level; + } + + /** + * Sets the level. + * <p> + * Valid arguments are: + * <ul> + * <li>{@link Severity#HIDDEN} + * <li>{@link Severity#WARNING} + * <li>{@link Severity#ERROR} + * </ul> + * + * @param level the level to set + */ + public void setLevel(Severity level) { + if (level == INHERIT) { + throw new IllegalArgumentException("Error level may not be set to INHERIT"); + } + this.level = level; + } + + public String toString() { + return "Error #" + this.code + " (" + this.name + ")"; + } + } + + private static final List<Error> sErrors = new ArrayList<>(); + + // Errors for API verification + public static final Error PARSE_ERROR = new Error(1, ERROR); + public static final Error ADDED_PACKAGE = new Error(2, WARNING); + public static final Error ADDED_CLASS = new Error(3, WARNING); + public static final Error ADDED_METHOD = new Error(4, WARNING); + public static final Error ADDED_FIELD = new Error(5, WARNING); + public static final Error ADDED_INTERFACE = new Error(6, WARNING); + public static final Error REMOVED_PACKAGE = new Error(7, WARNING); + public static final Error REMOVED_CLASS = new Error(8, WARNING); + public static final Error REMOVED_METHOD = new Error(9, WARNING); + public static final Error REMOVED_FIELD = new Error(10, WARNING); + public static final Error REMOVED_INTERFACE = new Error(11, WARNING); + public static final Error CHANGED_STATIC = new Error(12, WARNING); + public static final Error ADDED_FINAL = new Error(13, WARNING); + public static final Error CHANGED_TRANSIENT = new Error(14, WARNING); + public static final Error CHANGED_VOLATILE = new Error(15, WARNING); + public static final Error CHANGED_TYPE = new Error(16, WARNING); + public static final Error CHANGED_VALUE = new Error(17, WARNING); + public static final Error CHANGED_SUPERCLASS = new Error(18, WARNING); + public static final Error CHANGED_SCOPE = new Error(19, WARNING); + public static final Error CHANGED_ABSTRACT = new Error(20, WARNING); + public static final Error CHANGED_THROWS = new Error(21, WARNING); + public static final Error CHANGED_NATIVE = new Error(22, HIDDEN); + public static final Error CHANGED_CLASS = new Error(23, WARNING); + public static final Error CHANGED_DEPRECATED = new Error(24, WARNING); + public static final Error CHANGED_SYNCHRONIZED = new Error(25, WARNING); + public static final Error ADDED_FINAL_UNINSTANTIABLE = new Error(26, WARNING); + public static final Error REMOVED_FINAL = new Error(27, WARNING); + public static final Error REMOVED_DEPRECATED_CLASS = new Error(28, REMOVED_CLASS); + public static final Error REMOVED_DEPRECATED_METHOD = new Error(29, REMOVED_METHOD); + public static final Error REMOVED_DEPRECATED_FIELD = new Error(30, REMOVED_FIELD); + + + // Stuff I've added + public static final Error INVALID_NULL_CONVERSION = new Error(40, WARNING); + public static final Error PARAMETER_NAME_CHANGE = new Error(41, WARNING); + + + // Errors in javadoc generation + public static final Error UNRESOLVED_LINK = new Error(101, LINT); + public static final Error BAD_INCLUDE_TAG = new Error(102, LINT); + public static final Error UNKNOWN_TAG = new Error(103, LINT); + public static final Error UNKNOWN_PARAM_TAG_NAME = new Error(104, LINT); + public static final Error UNDOCUMENTED_PARAMETER = new Error(105, HIDDEN); // LINT + public static final Error BAD_ATTR_TAG = new Error(106, LINT); + public static final Error BAD_INHERITDOC = new Error(107, HIDDEN); // LINT + public static final Error HIDDEN_LINK = new Error(108, LINT); + public static final Error HIDDEN_CONSTRUCTOR = new Error(109, WARNING); + public static final Error UNAVAILABLE_SYMBOL = new Error(110, WARNING); + public static final Error HIDDEN_SUPERCLASS = new Error(111, WARNING); + public static final Error DEPRECATED = new Error(112, HIDDEN); + public static final Error DEPRECATION_MISMATCH = new Error(113, WARNING); + public static final Error MISSING_COMMENT = new Error(114, LINT); + public static final Error IO_ERROR = new Error(115, ERROR); + public static final Error NO_SINCE_DATA = new Error(116, HIDDEN); + public static final Error NO_FEDERATION_DATA = new Error(117, WARNING); + public static final Error BROKEN_SINCE_FILE = new Error(118, ERROR); + public static final Error INVALID_CONTENT_TYPE = new Error(119, ERROR); + public static final Error INVALID_SAMPLE_INDEX = new Error(120, ERROR); + public static final Error HIDDEN_TYPE_PARAMETER = new Error(121, WARNING); + public static final Error PRIVATE_SUPERCLASS = new Error(122, WARNING); + public static final Error NULLABLE = new Error(123, HIDDEN); // LINT + public static final Error INT_DEF = new Error(124, HIDDEN); // LINT + public static final Error REQUIRES_PERMISSION = new Error(125, LINT); + public static final Error BROADCAST_BEHAVIOR = new Error(126, LINT); + public static final Error SDK_CONSTANT = new Error(127, LINT); + public static final Error TODO = new Error(128, LINT); + public static final Error NO_ARTIFACT_DATA = new Error(129, HIDDEN); + public static final Error BROKEN_ARTIFACT_FILE = new Error(130, ERROR); + + // Metalava new warnings + public static final Error TYPO = new Error(131, LINT); + public static final Error MISSING_PERMISSION = new Error(132, LINT); + public static final Error MULTIPLE_THREAD_ANNOTATIONS = new Error(133, LINT); + public static final Error UNRESOLVED_CLASS = new Error(134, LINT); + + static { + // Attempt to initialize error names based on the field names + try { + for (Field field : Errors.class.getDeclaredFields()) { + Object o = field.get(null); + if (o instanceof Error) { + Error error = (Error) o; + String fieldName = field.getName(); + error.name = underlinesToCamelCase(fieldName.toLowerCase(Locale.US)); + } + } + } catch (Throwable unexpected) { + unexpected.printStackTrace(); + } + } + + public static boolean setErrorLevel(String id, Severity level) { + int code = -1; + if (Character.isDigit(id.charAt(0))) { + code = Integer.parseInt(id); + } + for (Error e : sErrors) { + if (e.code == code || e.name.equalsIgnoreCase(id)) { + e.setLevel(level); + return true; + } + } + return false; + } + + // Set error severity for all the compatibility related checks + public static void enforceCompatibility() { + for (Error e : sErrors) { + if (e.code >= Errors.PARSE_ERROR.code && e.code <= Errors.PARAMETER_NAME_CHANGE.code) { + e.setLevel(ERROR); + } + } + } + + // Primary needed by unit tests; ensure that a previous test doesn't influence + // a later one + public static void resetLevels() { + for (Error error : sErrors) { + error.level = error.defaultLevel; + } + } +} diff --git a/src/main/java/com/android/tools/metalava/doclava1/FilterPredicate.kt b/src/main/java/com/android/tools/metalava/doclava1/FilterPredicate.kt new file mode 100644 index 0000000..6fc7557 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/FilterPredicate.kt @@ -0,0 +1,35 @@ +/* + * 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.doclava1 + +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import java.util.function.Predicate + +// Ported from doclava1 + +class FilterPredicate(private val wrapped: Predicate<Item>) : Predicate<Item> { + + override fun test(method: Item): Boolean { + return when { + wrapped.test(method) -> true + method is MethodItem -> !method.isConstructor() && + method.findPredicateSuperMethod(wrapped) != null + else -> false + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/doclava1/SourcePositionInfo.java b/src/main/java/com/android/tools/metalava/doclava1/SourcePositionInfo.java new file mode 100644 index 0000000..8418742 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/SourcePositionInfo.java @@ -0,0 +1,66 @@ +/* + * 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.doclava1; + +// Copied from doclava1 +public class SourcePositionInfo implements Comparable { + public static final SourcePositionInfo UNKNOWN = new SourcePositionInfo("(unknown)", 0, 0); + + public SourcePositionInfo(String file, int line, int column) { + this.file = file; + this.line = line; + this.column = column; + } + + /** + * Given this position and str which occurs at that position, as well as str an index into str, + * find the SourcePositionInfo. + * + * @throw StringIndexOutOfBoundsException if index > str.length() + */ + public static SourcePositionInfo add(SourcePositionInfo that, String str, int index) { + if (that == null) { + return null; + } + int line = that.line; + char prev = 0; + for (int i = 0; i < index; i++) { + char c = str.charAt(i); + if (c == '\r' || (c == '\n' && prev != '\r')) { + line++; + } + prev = c; + } + return new SourcePositionInfo(that.file, line, 0); + } + + @Override + public String toString() { + return file + ':' + line; + } + + public int compareTo(Object o) { + SourcePositionInfo that = (SourcePositionInfo) o; + int r = this.file.compareTo(that.file); + if (r != 0) return r; + return this.line - that.line; + } + + public String file; + public int line; + public int column; +} diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt new file mode 100644 index 0000000..962320a --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2017 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 + +import com.android.SdkConstants +import com.android.SdkConstants.ATTR_VALUE +import com.android.tools.metalava.NEWLY_NONNULL +import com.android.tools.metalava.NEWLY_NULLABLE +import com.android.tools.metalava.Options +import com.android.tools.metalava.RECENTLY_NONNULL +import com.android.tools.metalava.RECENTLY_NULLABLE +import com.android.tools.metalava.options +import java.util.function.Predicate + +fun isNullableAnnotation(qualifiedName: String): Boolean { + return qualifiedName.endsWith("Nullable") +} + +fun isNonNullAnnotation(qualifiedName: String): Boolean { + return qualifiedName.endsWith("NonNull") || + qualifiedName.endsWith("NotNull") || + qualifiedName.endsWith("Nonnull") +} + +interface AnnotationItem { + val codebase: Codebase + + /** Fully qualified name of the annotation */ + fun qualifiedName(): String? + + /** Generates source code for this annotation (using fully qualified names) */ + fun toSource(): String + + /** Whether this annotation is significant and should be included in signature files, stubs, etc */ + fun isSignificant(): Boolean { + return isSignificantAnnotation(qualifiedName() ?: return false) + } + + /** Attributes of the annotation (may be empty) */ + fun attributes(): List<AnnotationAttribute> + + /** True if this annotation represents @Nullable or @NonNull (or some synonymous annotation) */ + fun isNullnessAnnotation(): Boolean { + return isNullable() || isNonNull() + } + + /** True if this annotation represents @Nullable (or some synonymous annotation) */ + fun isNullable(): Boolean { + return isNullableAnnotation(qualifiedName() ?: return false) + } + + /** True if this annotation represents @NonNull (or some synonymous annotation) */ + fun isNonNull(): Boolean { + return isNonNullAnnotation(qualifiedName() ?: return false) + } + + /** + * True if this annotation represents a @ParameterName annotation (or some synonymous annotation). + * The parameter name should be the default atttribute or "value". + */ + fun isParameterName(): Boolean { + return qualifiedName()?.endsWith(".ParameterName") ?: return false + } + + /** Returns the given named attribute if specified */ + fun findAttribute(name: String?): AnnotationAttribute? { + val actualName = name ?: ATTR_VALUE + return attributes().firstOrNull { it.name == actualName } + } + + /** Find the class declaration for the given annotation */ + fun resolve(): ClassItem? { + return codebase.findClass(qualifiedName() ?: return null) + } + + 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 + } + + /** The simple name of an annotation, which is the annotation name (not qualified name) prefixed by @ */ + fun simpleName(item: AnnotationItem): String { + val qualifiedName = item.qualifiedName() ?: return "" + return "@${qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)}" + } + + /** + * Maps an annotation name to the name to be used in signatures/stubs/external annotation files. + * Annotations that should not be exported are mapped to null. + */ + fun mapName(codebase: Codebase, qualifiedName: String?, filter: Predicate<Item>? = null): String? { + qualifiedName ?: return null + + when (qualifiedName) { + // Resource annotations + "android.annotation.AnimRes" -> return "android.support.annotation.AnimRes" + "android.annotation.AnimatorRes" -> return "android.support.annotation.AnimatorRes" + "android.annotation.AnyRes" -> return "android.support.annotation.AnyRes" + "android.annotation.ArrayRes" -> return "android.support.annotation.ArrayRes" + "android.annotation.AttrRes" -> return "android.support.annotation.AttrRes" + "android.annotation.BoolRes" -> return "android.support.annotation.BoolRes" + "android.annotation.ColorRes" -> return "android.support.annotation.ColorRes" + "android.annotation.DimenRes" -> return "android.support.annotation.DimenRes" + "android.annotation.DrawableRes" -> return "android.support.annotation.DrawableRes" + "android.annotation.FontRes" -> return "android.support.annotation.FontRes" + "android.annotation.FractionRes" -> return "android.support.annotation.FractionRes" + "android.annotation.IdRes" -> return "android.support.annotation.IdRes" + "android.annotation.IntegerRes" -> return "android.support.annotation.IntegerRes" + "android.annotation.InterpolatorRes" -> return "android.support.annotation.InterpolatorRes" + "android.annotation.LayoutRes" -> return "android.support.annotation.LayoutRes" + "android.annotation.MenuRes" -> return "android.support.annotation.MenuRes" + "android.annotation.PluralsRes" -> return "android.support.annotation.PluralsRes" + "android.annotation.RawRes" -> return "android.support.annotation.RawRes" + "android.annotation.StringRes" -> return "android.support.annotation.StringRes" + "android.annotation.StyleRes" -> return "android.support.annotation.StyleRes" + "android.annotation.StyleableRes" -> return "android.support.annotation.StyleableRes" + "android.annotation.TransitionRes" -> return "android.support.annotation.TransitionRes" + "android.annotation.XmlRes" -> return "android.support.annotation.XmlRes" + + // Threading + "android.annotation.AnyThread" -> return "android.support.annotation.AnyThread" + "android.annotation.BinderThread" -> return "android.support.annotation.BinderThread" + "android.annotation.MainThread" -> return "android.support.annotation.MainThread" + "android.annotation.UiThread" -> return "android.support.annotation.UiThread" + "android.annotation.WorkerThread" -> return "android.support.annotation.WorkerThread" + + // Colors + "android.annotation.ColorInt" -> return "android.support.annotation.ColorInt" + "android.annotation.ColorLong" -> return "android.support.annotation.ColorLong" + "android.annotation.HalfFloat" -> return "android.support.annotation.HalfFloat" + + // Ranges and sizes + "android.annotation.FloatRange" -> return "android.support.annotation.FloatRange" + "android.annotation.IntRange" -> return "android.support.annotation.IntRange" + "android.annotation.Size" -> return "android.support.annotation.Size" + "android.annotation.Px" -> return "android.support.annotation.Px" + "android.annotation.Dimension" -> return "android.support.annotation.Dimension" + + // Null + "android.annotation.NonNull" -> return "android.support.annotation.NonNull" + "android.annotation.Nullable" -> return "android.support.annotation.Nullable" + "libcore.util.NonNull" -> return "android.support.annotation.NonNull" + "libcore.util.Nullable" -> return "android.support.annotation.Nullable" + + // Typedefs + "android.annotation.IntDef" -> return "android.support.annotation.IntDef" + "android.annotation.StringDef" -> return "android.support.annotation.StringDef" + + // Misc + "android.annotation.CallSuper" -> return "android.support.annotation.CallSuper" + "android.annotation.CheckResult" -> return "android.support.annotation.CheckResult" + "android.annotation.RequiresPermission" -> return "android.support.annotation.RequiresPermission" + + // These aren't support annotations, but could/should be: + "android.annotation.CurrentTimeMillisLong", + "android.annotation.DurationMillisLong", + "android.annotation.ElapsedRealtimeLong", + "android.annotation.UserIdInt", + "android.annotation.BytesLong", + + // These aren't support annotations + "android.annotation.AppIdInt", + "android.annotation.BroadcastBehavior", + "android.annotation.SdkConstant", + "android.annotation.SuppressAutoDoc", + "android.annotation.SystemApi", + "android.annotation.TestApi", + "android.annotation.CallbackExecutor", + "android.annotation.Condemned", + + "android.annotation.Widget" -> { + // Remove, unless (a) public or (b) specifically included in --showAnnotations + return if (options.showAnnotations.contains(qualifiedName)) { + qualifiedName + } else if (filter != null) { + val cls = codebase.findClass(qualifiedName) + if (cls != null && filter.test(cls)) { + qualifiedName + } else { + null + } + } else { + qualifiedName + } + } + + // Included for analysis, but should not be exported: + "android.annotation.SystemService" -> return qualifiedName + + // Should not be mapped to a different package name: + "android.annotation.TargetApi", + "android.annotation.SuppressLint" -> return qualifiedName + + // We only change recently/newly nullable annotation if the codebase supports it + NEWLY_NULLABLE, RECENTLY_NULLABLE -> return if (codebase.supportsStagedNullability) qualifiedName else "android.support.annotation.Nullable" + NEWLY_NONNULL, RECENTLY_NONNULL -> return if (codebase.supportsStagedNullability) qualifiedName else "android.support.annotation.NonNull" + + else -> { + // Some new annotations added to the platform: assume they are support annotations? + return when { + // Special Kotlin annotations recognized by the compiler: map to supported package name + qualifiedName.endsWith(".ParameterName") || qualifiedName.endsWith(".DefaultValue") -> + "kotlin.annotations.jvm.internal${qualifiedName.substring(qualifiedName.lastIndexOf('.'))}" + + // Other third party nullness annotations? + isNullableAnnotation(qualifiedName) -> "android.support.annotation.Nullable" + 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 + + // Unknown Android platform annotations + qualifiedName.startsWith("android.annotation.") -> { + // Remove, unless specifically included in --showAnnotations + return if (options.showAnnotations.contains(qualifiedName)) { + qualifiedName + } else { + null + } + } + + else -> { + // Remove, unless (a) public or (b) specifically included in --showAnnotations + return if (options.showAnnotations.contains(qualifiedName)) { + qualifiedName + } else if (filter != null) { + val cls = codebase.findClass(qualifiedName) + if (cls != null && filter.test(cls)) { + qualifiedName + } else { + null + } + } else { + qualifiedName + } + } + } + } + } + } + + /** + * Given a "full" annotation name, shortens it by removing redundant package names. + * This is intended to be used by the [Options.omitCommonPackages] flag + * to reduce clutter in signature files. + * + * For example, this method will convert `@android.support.annotation.Nullable` to just + * `@Nullable`, and `@android.support.annotation.IntRange(from=20)` to `IntRange(from=20)`. + */ + fun shortenAnnotation(source: String): String { + return when { + source.startsWith("android.annotation.", 1) -> { + "@" + source.substring("@android.annotation.".length) + } + source.startsWith("android.support.annotation.", 1) -> { + "@" + source.substring("@android.support.annotation.".length) + } + else -> source + } + } + + /** + * Reverses the [shortenAnnotation] method. Intended for use when reading in signature files + * that contain shortened type references. + */ + fun unshortenAnnotation(source: String): String { + return when { + // These 3 annotations are in the android.annotation. package, not android.support.annotation + source.startsWith("@SystemService") || + source.startsWith("@TargetApi") || + source.startsWith("@SuppressLint") -> + "@android.annotation." + source.substring(1) + else -> { + "@android.support.annotation." + source.substring(1) + } + } + } + } +} + +/** An attribute of an annotation, such as "value" */ +interface AnnotationAttribute { + /** The name of the annotation */ + val name: String + /** The annotation value */ + val value: AnnotationAttributeValue + + /** + * Return all leaf values; this flattens the complication of handling + * {@code @SuppressLint("warning")} and {@code @SuppressLint({"warning1","warning2"}) + */ + fun leafValues(): List<AnnotationAttributeValue> { + val result = mutableListOf<AnnotationAttributeValue>() + AnnotationAttributeValue.addValues(value, result) + return result + } +} + +/** An annotation value */ +interface AnnotationAttributeValue { + /** Generates source code for this annotation value */ + fun toSource(): String + + /** The value of the annotation */ + fun value(): Any? + + /** If the annotation declaration references a field (or class etc), return the resolved class */ + fun resolve(): Item? + + companion object { + fun addValues(value: AnnotationAttributeValue, into: MutableList<AnnotationAttributeValue>) { + if (value is AnnotationArrayAttributeValue) { + for (v in value.values) { + addValues(v, into) + } + } else if (value is AnnotationSingleAttributeValue) { + into.add(value) + } + } + } +} + +/** An annotation value (for a single item, not an array) */ +interface AnnotationSingleAttributeValue : AnnotationAttributeValue { + /** The annotation value, expressed as source code */ + val valueSource: String + /** The annotation value */ + val value: Any? + + override fun value() = value +} + +/** An annotation value for an array of items */ +interface AnnotationArrayAttributeValue : AnnotationAttributeValue { + /** The annotation values */ + val values: List<AnnotationAttributeValue> + + override fun resolve(): Item? { + error("resolve() should not be called on an array value") + } + + override fun value() = values.mapNotNull { it.value() }.toTypedArray() +} + +class DefaultAnnotationAttribute( + override val name: String, + override val value: DefaultAnnotationValue +) : AnnotationAttribute { + companion object { + fun create(name: String, value: String): DefaultAnnotationAttribute { + return DefaultAnnotationAttribute(name, DefaultAnnotationValue.create(value)) + } + + fun createList(source: String): List<AnnotationAttribute> { + val list = mutableListOf<AnnotationAttribute>() + if (source.contains("{")) { + assert(source.indexOf('{', source.indexOf('{', source.indexOf('{') + 1) + 1) != -1, + { "Multiple arrays not supported: $source" }) + val split = source.indexOf('=') + val name: String + val value: String + if (split == -1) { + name = "value" + value = source.substring(source.indexOf('{')) + } else { + name = source.substring(0, split).trim() + value = source.substring(split + 1).trim() + } + list.add(DefaultAnnotationAttribute.create(name, value)) + return list + } + + source.split(",").forEach { declaration -> + val split = declaration.indexOf('=') + val name: String + val value: String + if (split == -1) { + name = "value" + value = declaration.trim() + } else { + name = declaration.substring(0, split).trim() + value = declaration.substring(split + 1).trim() + } + list.add(DefaultAnnotationAttribute.create(name, value)) + } + return list + } + } +} + +abstract class DefaultAnnotationValue : AnnotationAttributeValue { + companion object { + fun create(value: String): DefaultAnnotationValue { + return if (value.startsWith("{")) { // Array + DefaultAnnotationArrayAttributeValue(value) + } else { + DefaultAnnotationSingleAttributeValue(value) + } + } + } + + override fun toString(): String = toSource() +} + +class DefaultAnnotationSingleAttributeValue(override val valueSource: String) : DefaultAnnotationValue(), + AnnotationSingleAttributeValue { + @Suppress("IMPLICIT_CAST_TO_ANY") + override val value = when { + valueSource == SdkConstants.VALUE_TRUE -> true + valueSource == SdkConstants.VALUE_FALSE -> false + valueSource.startsWith("\"") -> valueSource.removeSurrounding("\"") + valueSource.startsWith('\'') -> valueSource.removeSurrounding("'")[0] + else -> try { + if (valueSource.contains(".")) { + valueSource.toDouble() + } else { + valueSource.toLong() + } + } catch (e: NumberFormatException) { + valueSource + } + } + + override fun resolve(): Item? = null + + override fun toSource() = valueSource +} + +class DefaultAnnotationArrayAttributeValue(val value: String) : DefaultAnnotationValue(), + AnnotationArrayAttributeValue { + init { + assert(value.startsWith("{") && value.endsWith("}"), { value }) + } + + override val values = value.substring(1, value.length - 1).split(",").map { + DefaultAnnotationValue.create(it.trim()) + }.toList() + + override fun toSource() = value +} diff --git a/src/main/java/com/android/tools/metalava/model/ClassItem.kt b/src/main/java/com/android/tools/metalava/model/ClassItem.kt new file mode 100644 index 0000000..18bdc7f --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.ApiAnalyzer +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import com.android.tools.metalava.options +import java.util.ArrayList +import java.util.LinkedHashSet +import java.util.function.Predicate + +interface ClassItem : Item { + /** The simple name of a class. In class foo.bar.Outer.Inner, the simple name is "Inner" */ + fun simpleName(): String + + /** The full name of a class. In class foo.bar.Outer.Inner, the full name is "Outer.Inner" */ + fun fullName(): String + + /** The qualified name of a class. In class foo.bar.Outer.Inner, the qualified name is the whole thing. */ + fun qualifiedName(): String + + /** Is this an innerclass? */ + fun isInnerClass(): Boolean = containingClass() != null + + /** Is this a top level class? */ + fun isTopLevelClass(): Boolean = containingClass() == null + + /** This [ClassItem] and all of its inner classes, recursively */ + fun allClasses(): Sequence<ClassItem> { + return sequenceOf(this).plus(innerClasses().asSequence().flatMap { it.allClasses() }) + } + + override fun parent(): Item? = containingClass() ?: containingPackage() + + /** + * The qualified name where inner classes use $ as a separator. + * In class foo.bar.Outer.Inner, this method will return foo.bar.Outer$Inner. + * (This is the name format used in ProGuard keep files for example.) + */ + fun qualifiedNameWithDollarInnerClasses(): String { + var curr: ClassItem? = this + while (curr?.containingClass() != null) { + curr = curr.containingClass() + } + + if (curr == null) { + return fullName().replace('.', '$') + } + + return curr.containingPackage().qualifiedName() + "." + fullName().replace('.', '$') + } + + /** Returns the internal name of the class, as seen in bytecode */ + fun internalName(): String { + var curr: ClassItem? = this + while (curr?.containingClass() != null) { + curr = curr.containingClass() + } + + if (curr == null) { + return fullName().replace('.', '$') + } + + return curr.containingPackage().qualifiedName().replace('.', '/') + "/" + + fullName().replace('.', '$') + } + + /** The super class of this class, if any */ + fun superClass(): ClassItem? + + /** The super class type of this class, if any. The difference between this and [superClass] is + * that the type reference can include type arguments; e.g. in "class MyList extends List<String>" + * the super class is java.util.List and the super class type is java.util.List<java.lang.String>. + * */ + fun superClassType(): TypeItem? + + /** Finds the public super class of this class, if any */ + fun publicSuperClass(): ClassItem? { + var superClass = superClass() + while (superClass != null && !superClass.checkLevel()) { + superClass = superClass.superClass() + } + + return superClass + } + + /** Any interfaces implemented by this class */ + fun interfaceTypes(): List<TypeItem> + + /** All classes and interfaces implemented (by this class and its super classes and the interfaces themselves) */ + fun allInterfaces(): Sequence<ClassItem> + + /** Any inner classes of this class */ + fun innerClasses(): List<ClassItem> + + /** The constructors in this class */ + fun constructors(): List<ConstructorItem> + + /** Whether this class has an implicit default constructor */ + fun hasImplicitDefaultConstructor(): Boolean + + /** The non-constructor methods in this class */ + fun methods(): List<MethodItem> + + /** The fields in this class */ + fun fields(): List<FieldItem> + + /** The members in this class: constructors, methods, fields/enum constants */ + fun members(): Sequence<MemberItem> { + return fields().asSequence().plus(constructors().asSequence()).plus(methods().asSequence()) + } + + /** Whether this class is an interface */ + fun isInterface(): Boolean + + /** Whether this class is an annotation type */ + fun isAnnotationType(): Boolean + + /** Whether this class is an enum */ + fun isEnum(): Boolean + + /** Whether this class is an interface */ + fun isClass(): Boolean = !isInterface() && !isAnnotationType() && !isEnum() + + /** The containing class, for inner classes */ + fun containingClass(): ClassItem? + + /** The containing package */ + fun containingPackage(): PackageItem + + /** Gets the type for this class */ + fun toType(): TypeItem + + /** Returns true if this class has type parameters */ + fun hasTypeVariables(): Boolean + + /** Any type parameters for the class, if any, as a source string (with fully qualified class names) */ + fun typeParameterList(): String? + + fun typeParameterNames(): List<String> + + /** Returns the classes that are part of the type parameters of this method, if any */ + fun typeArgumentClasses(): List<ClassItem> = TODO("Not yet implemented") + + fun isJavaLangObject(): Boolean { + return qualifiedName() == "java.lang.Object" + } + + // Mutation APIs: Used to "fix up" the API hierarchy (in [ApiAnalyzer]) to only expose + // visible parts of the API) + + // This replaces the "real" super class + fun setSuperClass(superClass: ClassItem?, superClassType: TypeItem? = superClass?.toType()) + + // This replaces the interface types implemented by this class + fun setInterfaceTypes(interfaceTypes: List<TypeItem>) + + val isTypeParameter: Boolean + + var hasPrivateConstructor: Boolean + + override fun accept(visitor: ItemVisitor) { + if (visitor is ApiVisitor) { + accept(visitor) + return + } + + if (visitor.skip(this)) { + return + } + + visitor.visitItem(this) + visitor.visitClass(this) + + for (constructor in constructors()) { + constructor.accept(visitor) + } + + for (method in methods()) { + method.accept(visitor) + } + + if (isEnum()) { + // In enums, visit the enum constants first, then the fields + for (field in fields()) { + if (field.isEnumConstant()) { + field.accept(visitor) + } + } + for (field in fields()) { + if (!field.isEnumConstant()) { + field.accept(visitor) + } + } + } else { + for (field in fields()) { + field.accept(visitor) + } + } + + if (visitor.nestInnerClasses) { + for (cls in innerClasses()) { + cls.accept(visitor) + } + } // otherwise done below + + visitor.afterVisitClass(this) + visitor.afterVisitItem(this) + + if (!visitor.nestInnerClasses) { + for (cls in innerClasses()) { + cls.accept(visitor) + } + } + } + + fun accept(visitor: ApiVisitor) { + if (visitor.skip(this)) { + return + } + + if (!visitor.include(this)) { + return + } + + // We build up a separate data structure such that we can compute the + // sets of fields, methods, etc even for inner classes (recursively); that way + // we can easily and up front determine whether we have any matches for + // inner classes (which is vital for computing the removed-api for example, where + // only something like the appearance of a removed method inside an inner class + // results in the outer class being described in the signature file. + val candidate = VisitCandidate(this, visitor) + candidate.accept() + } + + override fun acceptTypes(visitor: TypeVisitor) { + if (visitor.skip(this)) { + return + } + + val type = toType() + visitor.visitType(type, this) + + // TODO: Visit type parameter list (at least the bounds types, e.g. View in <T extends View> + superClass()?.let { + visitor.visitType(it.toType(), it) + } + + if (visitor.includeInterfaces) { + for (itf in interfaceTypes()) { + val owner = itf.asClass() + owner?.let { visitor.visitType(itf, it) } + } + } + + for (constructor in constructors()) { + constructor.acceptTypes(visitor) + } + for (field in fields()) { + field.acceptTypes(visitor) + } + for (method in methods()) { + method.acceptTypes(visitor) + } + for (cls in innerClasses()) { + cls.acceptTypes(visitor) + } + + visitor.afterVisitType(type, this) + } + + companion object { + // 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) { + o1.qualifiedName().compareTo(o2.qualifiedName()) + } else { + delta + } + } + + val nameComparator: Comparator<ClassItem> = Comparator { a, b -> + a.simpleName().compareTo(b.simpleName()) + } + + val fullNameComparator: Comparator<ClassItem> = Comparator { a, b -> a.fullName().compareTo(b.fullName()) } + + val qualifiedComparator: Comparator<ClassItem> = Comparator { a, b -> + a.qualifiedName().compareTo(b.qualifiedName()) + } + + fun classNameSorter(): Comparator<in ClassItem> = + if (compatibility.sortClassesBySimpleName) { + ClassItem.comparator + } else { + ClassItem.qualifiedComparator + } + } + + fun findMethod(methodName: String, parameters: String): MethodItem? + + fun findField(fieldName: String): FieldItem? + + /** Returns the corresponding compilation unit, if any */ + fun getCompilationUnit(): CompilationUnit? = null + + /** + * Return superclass matching the given predicate. When a superclass doesn't + * match, we'll keep crawling up the tree until we find someone who matches. + */ + fun filteredSuperclass(predicate: Predicate<Item>): ClassItem? { + val superClass = superClass() ?: return null + return if (predicate.test(superClass)) { + superClass + } else { + superClass.filteredSuperclass(predicate) + } + } + + fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? { + var superClassType: TypeItem? = superClassType() ?: return null + var prev: ClassItem? = null + while (superClassType != null) { + val superClass = superClassType.asClass() ?: return null + if (predicate.test(superClass)) { + if (prev == null || superClass == superClass()) { + // Direct reference; no need to map type variables + return superClassType + } + if (!superClassType.hasTypeArguments()) { + // No type variables - also no need for mapping + return superClassType + } + + return superClassType.convertType(this, prev) + } + + prev = superClass + superClassType = superClass.superClassType() + } + + return null + } + + /** + * Return methods matching the given predicate. Forcibly includes local + * methods that override a matching method in an ancestor class. + */ + fun filteredMethods(predicate: Predicate<Item>): Collection<MethodItem> { + val methods = LinkedHashSet<MethodItem>() + for (method in methods()) { + if (predicate.test(method) || method.findPredicateSuperMethod(predicate) != null) { + //val duplicated = method.duplicate(this) + //methods.add(duplicated) + methods.remove(method) + methods.add(method) + } + } + return methods + } + + /** Returns the constructors that match the given predicate */ + fun filteredConstructors(predicate: Predicate<Item>): Sequence<ConstructorItem> { + return constructors().asSequence().filter { predicate.test(it) } + } + + /** + * Return fields matching the given predicate. Also clones fields from + * ancestors that would match had they been defined in this class. + */ + fun filteredFields(predicate: Predicate<Item>): List<FieldItem> { + val fields = LinkedHashSet<FieldItem>() + if (options.showUnannotated) { + for (clazz in allInterfaces()) { + if (!clazz.isInterface()) { + continue + } + for (field in clazz.fields()) { + if (!predicate.test(field)) { + val clz = this + val duplicated = field.duplicate(clz) + if (predicate.test(duplicated)) { + fields.remove(duplicated) + fields.add(duplicated) + } + } + } + } + } + for (field in fields()) { + if (predicate.test(field)) { + fields.remove(field) + fields.add(field) + } + } + if (fields.isEmpty()) { + return emptyList() + } + val list = fields.toMutableList() + list.sortWith(FieldItem.comparator) + return list + } + + fun filteredInterfaceTypes(predicate: Predicate<Item>): Collection<TypeItem> { + val interfaceTypes = filteredInterfaceTypes( + predicate, LinkedHashSet(), + includeSelf = false, includeParents = false, target = this + ) + if (interfaceTypes.isEmpty()) { + return interfaceTypes + } + + return interfaceTypes + } + + fun allInterfaceTypes(predicate: Predicate<Item>): Collection<TypeItem> { + val interfaceTypes = filteredInterfaceTypes( + predicate, LinkedHashSet(), + includeSelf = false, includeParents = true, target = this + ) + if (interfaceTypes.isEmpty()) { + return interfaceTypes + } + + return interfaceTypes + } + + private fun filteredInterfaceTypes( + predicate: Predicate<Item>, + types: LinkedHashSet<TypeItem>, + includeSelf: Boolean, + includeParents: Boolean, + target: ClassItem + ): LinkedHashSet<TypeItem> { + val superClassType = superClassType() + if (superClassType != null) { + val superClass = superClassType.asClass() + if (superClass != null) { + if (!predicate.test(superClass)) { + superClass.filteredInterfaceTypes(predicate, types, true, includeParents, target) + } else if (includeSelf && superClass.isInterface()) { + types.add(superClassType) + if (includeParents) { + superClass.filteredInterfaceTypes(predicate, types, true, includeParents, target) + } + } + } + } + for (type in interfaceTypes()) { + val cls = type.asClass() ?: continue + if (predicate.test(cls)) { + if (hasTypeVariables() && type.hasTypeArguments()) { + val replacementMap = target.mapTypeVariables(this) + if (replacementMap.isNotEmpty()) { + val mapped = type.convertType(replacementMap) + types.add(mapped) + continue + } + } + types.add(type) + if (includeParents) { + cls.filteredInterfaceTypes(predicate, types, true, includeParents, target) + } + } else { + cls.filteredInterfaceTypes(predicate, types, true, includeParents, target) + } + } + return types + } + + fun allInnerClasses(includeSelf: Boolean = false): Sequence<ClassItem> { + val list = ArrayList<ClassItem>() + if (includeSelf) { + list.add(this) + } + addInnerClasses(list, this) + return list.asSequence() + } + + private fun addInnerClasses(list: MutableList<ClassItem>, cls: ClassItem) { + for (innerClass in cls.innerClasses()) { + list.add(innerClass) + addInnerClasses(list, innerClass) + } + } + + /** + * The default constructor to invoke on this class from subclasses; initially null + * but populated by [ApiAnalyzer.addConstructors]. (Note that in some cases + * [defaultConstructor] may not be in [constructors], e.g. when we need to + * create a constructor to match a public parent class with a non-default constructor + * and the one in the code is not a match, e.g. is marked @hide etc.) + */ + var defaultConstructor: ConstructorItem? + + /** + * Creates a map of type variables from this class to the given target class. + * If class A<X,Y> extends B<X,Y>, and B is declared as class B<M,N>, + * this returns the map {"X"->"M", "Y"->"N"}. There could be multiple intermediate + * classes between this class and the target class, and in some cases we could + * be substituting in a concrete class, e.g. class MyClass extends B<String,Number> + * would return the map {"java.lang.String"->"M", "java.lang.Number"->"N"}. + * + * If [reverse] is true, compute the reverse map: keys are the variables in + * the target and the values are the variables in the source. + */ + fun mapTypeVariables(target: ClassItem, reverse: Boolean = false): Map<String, String> = codebase.unsupported() + + /** + * Creates a constructor in this class + */ + fun createDefaultConstructor(): ConstructorItem = codebase.unsupported() + + /** + * Creates a method corresponding to the given method signature in this class + */ + fun createMethod(template: MethodItem): MethodItem = codebase.unsupported() + + fun addMethod(method: MethodItem): Unit = codebase.unsupported() +} + +class VisitCandidate(private val cls: ClassItem, private val visitor: ApiVisitor) { + private val innerClasses: Sequence<VisitCandidate> + private val constructors: Sequence<MethodItem> + private val methods: Sequence<MethodItem> + private val fields: Sequence<FieldItem> + private val enums: Sequence<FieldItem> + + init { + val filterEmit = visitor.filterEmit + + constructors = cls.constructors().asSequence() + .filter { filterEmit.test(it) } + .sortedWith(MethodItem.comparator) + + methods = cls.methods().asSequence() + .filter { filterEmit.test(it) } + .sortedWith(MethodItem.comparator) + + val fieldSequence = + if (visitor.inlineInheritedFields) { + cls.filteredFields(filterEmit).asSequence() + } else { + cls.fields().asSequence() + } + if (cls.isEnum()) { + fields = fieldSequence + .filter({ !it.isEnumConstant() }) + .sortedWith(FieldItem.comparator) + enums = fieldSequence + .filter({ it.isEnumConstant() }) + .filter { filterEmit.test(it) } + .sortedWith(FieldItem.comparator) + } else { + fields = fieldSequence.sortedWith(FieldItem.comparator) + enums = emptySequence() + } + + innerClasses = cls.innerClasses() + .asSequence() + .sortedWith(ClassItem.classNameSorter()) + .map { VisitCandidate(it, visitor) } + } + + /** Will this class emit anything? */ + private fun emit(): Boolean { + val emit = emitClass() + if (emit) { + return true + } + + return innerClasses.any { it.emit() } + } + + /** Does the body of this class (everything other than the inner classes) emit anything? */ + private fun emitClass(): Boolean { + val classEmpty = (constructors.none() && methods.none() && enums.none() && fields.none()) + return if (visitor.filterEmit.test(cls)) { + true + } else if (!classEmpty) { + visitor.filterReference.test(cls) + } else { + false + } + } + + fun accept() { + if (visitor.skip(cls)) { + return + } + + if (!visitor.include(cls)) { + return + } + + if (!emit()) { + return + } + + val emitClass = emitClass() + + if (emitClass) { + if (!visitor.visitingPackage) { + visitor.visitingPackage = true + val pkg = cls.containingPackage() + visitor.visitItem(pkg) + visitor.visitPackage(pkg) + } + + visitor.visitItem(cls) + visitor.visitClass(cls) + + val sortedConstructors = if (visitor.methodComparator != null) { + constructors.sortedWith(visitor.methodComparator) + } else { + constructors + } + val sortedMethods = if (visitor.methodComparator != null) { + methods.sortedWith(visitor.methodComparator) + } else { + methods + } + val sortedFields = if (visitor.fieldComparator != null) { + fields.sortedWith(visitor.fieldComparator) + } else { + fields + } + + + for (constructor in sortedConstructors) { + constructor.accept(visitor) + } + + for (method in sortedMethods) { + method.accept(visitor) + } + + for (enumConstant in enums) { + enumConstant.accept(visitor) + } + for (field in sortedFields) { + field.accept(visitor) + } + } + + if (visitor.nestInnerClasses) { // otherwise done below + innerClasses.forEach { it.accept() } + } + + if (emitClass) { + visitor.afterVisitClass(cls) + visitor.afterVisitItem(cls) + } + + if (!visitor.nestInnerClasses) { + innerClasses.forEach { it.accept() } + } + } +} diff --git a/src/main/java/com/android/tools/metalava/model/Codebase.kt b/src/main/java/com/android/tools/metalava/model/Codebase.kt new file mode 100644 index 0000000..3cb8d54 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/Codebase.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2017 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 + +import com.android.SdkConstants.ANDROID_URI +import com.android.SdkConstants.ATTR_NAME +import com.android.SdkConstants.TAG_PERMISSION +import com.android.tools.metalava.CodebaseComparator +import com.android.tools.metalava.ComparisonVisitor +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.model.text.TextBackedAnnotationItem +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import com.android.tools.metalava.reporter +import com.android.utils.XmlUtils +import com.android.utils.XmlUtils.getFirstSubTagByName +import com.android.utils.XmlUtils.getNextTagByName +import com.intellij.psi.PsiFile +import org.intellij.lang.annotations.Language +import java.io.File +import java.util.function.Predicate +import kotlin.text.Charsets.UTF_8 + +/** + * Represents a complete unit of code -- typically in the form of a set + * of source trees, but also potentially backed by .jar files or even + * signature files + */ +interface Codebase { + /** Description of what this codebase is (useful during debugging) */ + var description: String + + /** The packages in the codebase (may include packages that are not included in the API) */ + fun getPackages(): PackageList + + /** The rough size of the codebase (package count) */ + fun size(): Int + + /** Returns a class identified by fully qualified name, if in the codebase */ + fun findClass(className: String): ClassItem? + + /** Returns a package identified by fully qualifiedname, if in the codebase */ + fun findPackage(pkgName: String): PackageItem? + + /** Returns true if this codebase supports documentation. */ + fun supportsDocumentation(): Boolean + + /** + * Returns true if this codebase corresponds to an already trusted API (e.g. + * is read in from something like an existing signature file); in that case, + * signature checks etc will not be performed. + */ + fun trustedApi(): Boolean + + fun accept(visitor: ItemVisitor) { + getPackages().accept(visitor) + } + + fun acceptTypes(visitor: TypeVisitor) { + getPackages().acceptTypes(visitor) + } + + /** + * Visits this codebase and compares it with another codebase, informing the visitors about + * the correlations and differences that it finds + */ + fun compareWith(visitor: ComparisonVisitor, other: Codebase, filter: Predicate<Item>? = null) { + CodebaseComparator().compare(visitor, other, this, filter) + } + + /** + * Creates an annotation item for the given (fully qualified) Java source + */ + fun createAnnotation( + @Language("JAVA") source: String, context: Item? = null, + mapName: Boolean = true + ): AnnotationItem = TextBackedAnnotationItem( + this, source, mapName + ) + + /** + * Returns true if the codebase contains one or more Kotlin files + */ + fun hasKotlin(): Boolean { + return units.any { it.fileType.name == "Kotlin" } + } + + /** + * Returns true if the codebase contains one or more Java files + */ + fun hasJava(): Boolean { + return units.any { it.fileType.name == "JAVA" } + } + + /** The manifest to associate with this codebase, if any */ + var manifest: File? + + /** + * Returns the permission level of the named permission, if specified + * in the manifest. This method should only be called if the codebase has + * been configured with a manifest + */ + fun getPermissionLevel(name: String): String? + + /** Clear the [Item.tag] fields (prior to iteration like DFS) */ + fun clearTags() { + getPackages().packages.forEach { pkg -> pkg.allClasses().forEach { cls -> cls.tag = false } } + } + + /** + * Creates a filtered version of this codebase + */ + fun filter(filterEmit: Predicate<Item>, filterReference: Predicate<Item>): Codebase + + /** Reports that the given operation is unsupported for this codebase type */ + fun unsupported(desc: String? = null): Nothing + + /** Whether this codebase supports staged nullability (RecentlyNullable etc) */ + var supportsStagedNullability: Boolean + + /** If this codebase was filtered from another codebase, this points to the original */ + var original: Codebase? + + /** Returns the compilation units used in this codebase (may be empty + * when the codebase is not loaded from source, such as from .jar files or + * from signature files) */ + var units: List<PsiFile> +} + +abstract class DefaultCodebase : Codebase { + override var manifest: File? = null + private var permissions: Map<String, String>? = null + override var original: Codebase? = null + override var supportsStagedNullability: Boolean = false + override var units: List<PsiFile> = emptyList() + + override fun getPermissionLevel(name: String): String? { + if (permissions == null) { + assert(manifest != null, + { "This method should only be called when a manifest has been configured on the codebase" }) + try { + val map = HashMap<String, String>(600) + val doc = XmlUtils.parseDocument(manifest?.readText(UTF_8), true) + var current = getFirstSubTagByName(doc.documentElement, TAG_PERMISSION) + while (current != null) { + val permissionName = current.getAttributeNS(ANDROID_URI, ATTR_NAME) + val protectionLevel = current.getAttributeNS(ANDROID_URI, "protectionLevel") + map.put(permissionName, protectionLevel) + current = getNextTagByName(current, TAG_PERMISSION) + } + permissions = map + } catch (error: Throwable) { + reporter.report(Errors.PARSE_ERROR, manifest, "Failed to parse $manifest: ${error.message}") + permissions = emptyMap() + } + } + + return permissions!![name] + } + + override fun unsupported(desc: String?): Nothing { + error(desc ?: "This operation is not available on this type of codebase (${this.javaClass.simpleName})") + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt b/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt new file mode 100644 index 0000000..6f75493 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 + +import com.intellij.lang.Language +import com.intellij.psi.PsiFile +import java.util.function.Predicate + +/** Represents a compilation unit (e.g. a .java or a .kt file) */ +open class CompilationUnit( + val file: PsiFile +) { + + val language: Language? get() = file.language + + open fun getHeaderComments(): String? = null + + override fun toString(): String = "compilation unit ${file.virtualFile?.path}" + + open fun getImportStatements(predicate: Predicate<Item>): Collection<String>? = null +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/ConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/ConstructorItem.kt new file mode 100644 index 0000000..68eb476 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ConstructorItem.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 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 + +interface ConstructorItem : MethodItem { + override fun isConstructor(): Boolean = true + + /** Returns the internal name of the class, as seen in bytecode */ + override fun internalName(): String = "<init>" + + /** + * The constructor that this method delegates to initially (e.g. super- or this- or default/implicit null + * constructor). Note that it may not be in a super class, as in the case of a this-call. + */ + var superConstructor: ConstructorItem? +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/FieldItem.kt b/src/main/java/com/android/tools/metalava/model/FieldItem.kt new file mode 100644 index 0000000..1dbd8d1 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/FieldItem.kt @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import java.io.PrintWriter + +interface FieldItem : MemberItem { + /** The type of this field */ + fun type(): TypeItem + + /** + * The initial/constant value, if any. If [requireConstant] the initial value will + * only be returned if it's constant. + */ + fun initialValue(requireConstant: Boolean = true): Any? + + /** + * An enum can contain both enum constants and fields; this method provides a way + * to distinguish between them. + */ + fun isEnumConstant(): Boolean + + /** + * Duplicates this field item. Used when we need to insert inherited fields from + * interfaces etc. + */ + fun duplicate(targetContainingClass: ClassItem): FieldItem + + override fun accept(visitor: ItemVisitor) { + if (visitor.skip(this)) { + return + } + + visitor.visitItem(this) + visitor.visitField(this) + + visitor.afterVisitField(this) + visitor.afterVisitItem(this) + } + + override fun acceptTypes(visitor: TypeVisitor) { + if (visitor.skip(this)) { + return + } + + val type = type() + visitor.visitType(type, this) + visitor.afterVisitType(type, this) + } + + companion object { + val comparator: java.util.Comparator<FieldItem> = Comparator { a, b -> a.name().compareTo(b.name()) } + } + + /** + * If this field has an initial value, it just writes ";", otherwise it writes + * " = value;" with the correct Java syntax for the initial value + */ + fun writeValueWithSemicolon( + writer: PrintWriter, + allowDefaultValue: Boolean = false, + requireInitialValue: Boolean = false + ) { + val value = + initialValue(!allowDefaultValue) + ?: if (allowDefaultValue && !containingClass().isClass()) type().defaultValue() else null + if (value != null) { + when (value) { + is Int -> { + writer.print(" = ") + writer.print(value) + writer.print("; // 0x") + writer.print(Integer.toHexString(value)) + } + is String -> { + writer.print(" = ") + writer.print('"') + writer.print(javaEscapeString(value)) + writer.print('"') + writer.print(";") + } + is Long -> { + writer.print(" = ") + writer.print(value) + writer.print(String.format("L; // 0x%xL", value)) + } + is Boolean -> { + writer.print(" = ") + writer.print(value) + writer.print(";") + } + is Byte -> { + writer.print(" = ") + writer.print(value) + writer.print("; // 0x") + writer.print(Integer.toHexString(value.toInt())) + } + is Short -> { + writer.print(" = ") + writer.print(value) + writer.print("; // 0x") + writer.print(Integer.toHexString(value.toInt())) + } + is Float -> { + writer.print(" = ") + when (value) { + Float.POSITIVE_INFINITY -> writer.print("(1.0f/0.0f);") + Float.NEGATIVE_INFINITY -> writer.print("(-1.0f/0.0f);") + Float.NaN -> writer.print("(0.0f/0.0f);") + else -> { + writer.print(canonicalizeFloatingPointString(value.toString())) + writer.print("f;") + } + } + } + is Double -> { + writer.print(" = ") + when (value) { + Double.POSITIVE_INFINITY -> writer.print("(1.0/0.0);") + Double.NEGATIVE_INFINITY -> writer.print("(-1.0/0.0);") + Double.NaN -> writer.print("(0.0/0.0);") + else -> { + writer.print(canonicalizeFloatingPointString(value.toString())) + writer.print(";") + } + } + } + is Char -> { + writer.print(" = ") + val intValue = value.toInt() + writer.print(intValue) + writer.print("; // ") + writer.print( + String.format( + "0x%04x '%s'", intValue, + javaEscapeString(value.toString()) + ) + ) + } + else -> { + writer.print(';') + } + } + } else { + // in interfaces etc we must have an initial value + if (requireInitialValue && !containingClass().isClass()) { + writer.print(" = null") + } + writer.print(';') + } + } +} + +fun javaEscapeString(str: String): String { + var result = "" + val n = str.length + for (i in 0 until n) { + val c = str[i] + result += when (c) { + '\\' -> "\\\\" + '\t' -> "\\t" + '\b' -> "\\b" + '\r' -> "\\r" + '\n' -> "\\n" + '\'' -> "\\'" + '\"' -> "\\\"" + in ' '..'~' -> c + else -> String.format("\\u%04x", c.toInt()) + } + } + return result +} + +// From doclava1 TextFieldItem#javaUnescapeString +fun javaUnescapeString(str: String): String { + val n = str.length + var simple = true + for (i in 0 until n) { + val c = str[i] + if (c == '\\') { + simple = false + break + } + } + if (simple) { + return str + } + + val buf = StringBuilder(str.length) + var escaped: Char = 0.toChar() + val START = 0 + val CHAR1 = 1 + val CHAR2 = 2 + val CHAR3 = 3 + val CHAR4 = 4 + val ESCAPE = 5 + var state = START + + for (i in 0 until n) { + val c = str[i] + when (state) { + START -> if (c == '\\') { + state = ESCAPE + } else { + buf.append(c) + } + ESCAPE -> when (c) { + '\\' -> { + buf.append('\\') + state = START + } + 't' -> { + buf.append('\t') + state = START + } + 'b' -> { + buf.append('\b') + state = START + } + 'r' -> { + buf.append('\r') + state = START + } + 'n' -> { + buf.append('\n') + state = START + } + '\'' -> { + buf.append('\'') + state = START + } + '\"' -> { + buf.append('\"') + state = START + } + 'u' -> { + state = CHAR1 + escaped = 0.toChar() + } + } + CHAR1, CHAR2, CHAR3, CHAR4 -> { + + escaped = (escaped.toInt() shl 4).toChar() + escaped = when (c) { + in '0'..'9' -> (escaped.toInt() or (c - '0')).toChar() + in 'a'..'f' -> (escaped.toInt() or (10 + (c - 'a'))).toChar() + in 'A'..'F' -> (escaped.toInt() or (10 + (c - 'A'))).toChar() + else -> throw IllegalArgumentException( + "bad escape sequence: '" + c + "' at pos " + i + " in: \"" + + str + "\"" + ) + } + if (state == CHAR4) { + buf.append(escaped) + state = START + } else { + state++ + } + } + } + } + if (state != START) { + throw IllegalArgumentException("unfinished escape sequence: " + str) + } + return buf.toString() +} + +/** + * Returns a canonical string representation of a floating point + * number. The representation is suitable for use as Java source + * code. This method also addresses bug #4428022 in the Sun JDK. + */ +// From doclava1 +fun canonicalizeFloatingPointString(value: String): String { + var str = value + if (str.indexOf('E') != -1) { + return str + } + + // 1.0 is the only case where a trailing "0" is allowed. + // 1.00 is canonicalized as 1.0. + var i = str.length - 1 + val d = str.indexOf('.') + while (i >= d + 2 && str[i] == '0') { + str = str.substring(0, i--) + } + return str +} diff --git a/src/main/java/com/android/tools/metalava/model/Item.kt b/src/main/java/com/android/tools/metalava/model/Item.kt new file mode 100644 index 0000000..7197729 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/Item.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import com.intellij.psi.PsiElement + +/** + * Represents a code element such as a package, a class, a method, a field, a parameter. + * + * This extra abstraction on top of PSI allows us to more model the API (and customize + * visibility, which cannot always be done by looking at a particular piece of code and examining + * visibility and @hide/@removed annotations: sometimes package private APIs are unhidden by + * being used in public APIs for example. + * + * The abstraction also lets us back the model by an alternative implementation read from + * signature files, to do compatibility checks. + * */ +interface Item { + val codebase: Codebase + + /** Return the modifiers of this class */ + val modifiers: ModifierList + + /** + * Whether this element should be part of the API. The algorithm for this is complicated, so it can't + * be computed initially; we'll make passes over the source code to determine eligibility and mark all + * items as included or not. + */ + var included: Boolean + + /** Whether this element has been hidden with @hide/@Hide (or after propagation, in some containing class/pkg) */ + var hidden: Boolean + + var emit: Boolean + + fun parent(): Item? + + /** Recursive check to see if this item or any of its parents (containing class, containing package) are hidden */ + fun hidden(): Boolean { + return hidden || parent()?.hidden() ?: false + } + + /** Whether this element has been removed with @removed/@Remove (or after propagation, in some containing class) */ + var removed: Boolean + + /** True if this element has been marked deprecated */ + val deprecated: Boolean + + /** True if this element is only intended for documentation */ + var docOnly: Boolean + + /** True if this item is either hidden or removed */ + fun isHiddenOrRemoved(): Boolean = hidden || removed + + /** Visits this element using the given [visitor] */ + fun accept(visitor: ItemVisitor) + + /** Visits all types in this item hierarchy */ + fun acceptTypes(visitor: TypeVisitor) + + /** Get a mutable version of modifiers for this item */ + fun mutableModifiers(): MutableModifierList + + /** The javadoc/KDoc comment for this code element, if any. This is + * the original content of the documentation, including lexical tokens + * to begin, continue and end the comment (such as /+*). + * See [fullyQualifiedDocumentation] to look up the documentation with + * fully qualified references to classes. + */ + var documentation: String + + /** Looks up docs for a specific tag */ + fun findTagDocumentation(tag: String): String? + + /** + * A rank used for sorting. This allows signature files etc to + * sort similar items by a natural order, if non-zero. + * (Even though in signature files the elements are normally + * sorted first logically (constructors, then methods, then fields) + * and then alphabetically, this lets us preserve the source + * ordering for example for overloaded methods of the same name, + * where it's not clear that an alphabetical order (of each + * parameter?) would be preferable.) + */ + val sortingRank: Int + + /** + * Add the given text to the documentation. + * + * If the [tagSection] is null, add the comment to the initial text block + * of the description. Otherwise if it is "@return", add the comment + * to the return value. Otherwise the [tagSection] is taken to be the + * parameter name, and the comment added as parameter documentation + * for the given parameter. + */ + fun appendDocumentation(comment: String, tagSection: String? = null, append: Boolean = true) + + val isPublic: Boolean + val isProtected: Boolean + val isPackagePrivate: Boolean + val isPrivate: Boolean + + // make sure these are implemented so we can place in maps: + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int + + /** + * Returns true if this item requires nullness information (e.g. for a method + * where either the return value or any of the parameters are non-primitives. + * Note that it doesn't consider whether it already has nullness annotations; + * for that see [hasNullnessInfo]. + */ + fun requiresNullnessInfo(): Boolean { + return false + } + + /** + * Whether this item was loaded from the classpath (e.g. jar dependencies) + * rather than be declared as source + */ + fun isFromClassPath(): Boolean = false + + /** + * Returns true if this item requires nullness information and supplies it + * (for all items, e.g. if a method is partially annotated this method would + * still return false) + */ + fun hasNullnessInfo(): Boolean { + when (this) { + is ParameterItem -> { + return !type().primitive + } + + is MethodItem -> { + val returnType = returnType() + if (returnType != null && !returnType.primitive) { + return true + } + for (parameter in parameters()) { + if (!parameter.type().primitive) { + return true + } + } + return false + } + } + + return false + } + + fun hasShowAnnotation(): Boolean = modifiers.hasShowAnnotation() + fun hasHideAnnotation(): Boolean = modifiers.hasHideAnnotations() + + // TODO: Cache? + fun checkLevel(): Boolean { + if (isHiddenOrRemoved()) { + return false + } + return modifiers.isPublic() || modifiers.isProtected() + } + + fun compilationUnit(): CompilationUnit? { + var curr: Item? = this + while (curr != null) { + if (curr is ClassItem && curr.isTopLevelClass()) { + return curr.getCompilationUnit() + } + curr = curr.parent() + } + + return null + } + + /** Returns the PSI element for this item, if any */ + fun psi(): PsiElement? = null + + /** Tag field used for DFS etc */ + var tag: Boolean + + /** + * Returns the [documentation], but with fully qualified links (except for the same package, and + * when turning a relative reference into a fully qualified reference, use the javadoc syntax + * for continuing to display the relative text, e.g. instead of {@link java.util.List}, use + * {@link java.util.List List}. + */ + fun fullyQualifiedDocumentation(): String = documentation +} + +abstract class DefaultItem(override val sortingRank: Int = nextRank++) : Item { + override val isPublic: Boolean get() = modifiers.isPublic() + override val isProtected: Boolean get() = modifiers.isProtected() + override val isPackagePrivate: Boolean get() = modifiers.isPackagePrivate() + override val isPrivate: Boolean get() = modifiers.isPrivate() + + override var emit = true + override var tag: Boolean = false + + // TODO: Get rid of this; with the new predicate approach it's redundant (and + // storing it per element is problematic since the predicate sometimes includes + // methods from parent interfaces etc) + override var included: Boolean = true + + companion object { + private var nextRank: Int = 1 + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/MemberItem.kt b/src/main/java/com/android/tools/metalava/model/MemberItem.kt new file mode 100644 index 0000000..ecf9d57 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/MemberItem.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 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 + +interface MemberItem : Item { + /** The name of this method/field. Constructors have the same name as their containing class' simple name */ + fun name(): String + + /** Returns the internal name of the method, as seen in bytecode */ + fun internalName(): String = name() + + /** The containing class */ + fun containingClass(): ClassItem + + override fun parent(): ClassItem? = containingClass() +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/src/main/java/com/android/tools/metalava/model/MethodItem.kt new file mode 100644 index 0000000..2e5823d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import java.util.LinkedHashSet +import java.util.function.Predicate + +interface MethodItem : MemberItem { + /** Whether this method is a constructor */ + fun isConstructor(): Boolean + + /** The type of this field, or null for constructors */ + fun returnType(): TypeItem? + + /** The list of parameters */ + fun parameters(): List<ParameterItem> + + /** Returns the super methods that this method is overriding */ + fun superMethods(): List<MethodItem> + + fun allSuperMethods(): Sequence<MethodItem> { + val original = superMethods().firstOrNull() ?: return emptySequence() + return generateSequence(original) { item -> + val superMethods = item.superMethods() + superMethods.firstOrNull() + } + } + + /** Any type parameters for the class, if any, as a source string (with fully qualified class names) */ + fun typeParameterList(): String? + + /** Returns the classes that are part of the type parameters of this method, if any */ + fun typeArgumentClasses(): List<ClassItem> = TODO("Not yet implemented") + + /** Types of exceptions that this method can throw */ + fun throwsTypes(): List<ClassItem> + + fun filteredThrowsTypes(predicate: Predicate<Item>): Collection<ClassItem> { + return filteredThrowsTypes(predicate, LinkedHashSet()) + } + + private fun filteredThrowsTypes( + predicate: Predicate<Item>, + classes: LinkedHashSet<ClassItem> + ): LinkedHashSet<ClassItem> { + + for (cls in throwsTypes()) { + if (predicate.test(cls)) { + classes.add(cls) + } else { + // Excluded, but it may have super class throwables that are included; if so, include those + var curr = cls.publicSuperClass() + while (curr != null) { + if (predicate.test(cls)) { + classes.add(curr) + break + } + curr = curr.publicSuperClass() + } + } + } + return classes + } + + /** + * If this method is inherited from a hidden super class, but implements a method + * from a public interface, this property is set. This is necessary because these + * methods should not be listed in signature files (at least not in compatibility mode), + * whereas in stub files it's necessary for them to be included (otherwise subclasses + * may think the method required and not yet implemented, e.g. the class must be + * abstract.) + */ + var inheritedInterfaceMethod: Boolean + + /** + * Duplicates this field item. Used when we need to insert inherited fields from + * interfaces etc. + */ + fun duplicate(targetContainingClass: ClassItem): MethodItem + + fun findPredicateSuperMethod(predicate: Predicate<Item>): MethodItem? { + if (isConstructor()) { + return null + } + + val superMethods = superMethods() + for (method in superMethods) { + if (predicate.test(method)) { + return method + } + } + + for (method in superMethods) { + val found = method.findPredicateSuperMethod(predicate) + if (found != null) { + return found + } + } + + return null + } + + override fun accept(visitor: ItemVisitor) { + if (visitor.skip(this)) { + return + } + + visitor.visitItem(this) + if (isConstructor()) { + visitor.visitConstructor(this as ConstructorItem) + } else { + visitor.visitMethod(this) + } + + for (parameter in parameters()) { + parameter.accept(visitor) + } + + if (isConstructor()) { + visitor.afterVisitConstructor(this as ConstructorItem) + } else { + visitor.afterVisitMethod(this) + } + visitor.afterVisitItem(this) + } + + override fun acceptTypes(visitor: TypeVisitor) { + if (visitor.skip(this)) { + return + } + + if (!isConstructor()) { + val type = returnType() + if (type != null) { // always true when not a constructor + visitor.visitType(type, this) + } + } + + for (parameter in parameters()) { + parameter.acceptTypes(visitor) + } + + for (exception in throwsTypes()) { + exception.acceptTypes(visitor) + } + + if (!isConstructor()) { + val type = returnType() + if (type != null) { + visitor.visitType(type, this) + } + } + } + + companion object { + private fun compareMethods(o1: MethodItem, o2: MethodItem): Int { + val name1 = o1.name() + val name2 = o2.name() + if (name1 == name2) { + val rankDelta = o1.sortingRank - o2.sortingRank + if (rankDelta != 0) { + return rankDelta + } + + // Compare by the rest of the signature to ensure stable output (we don't need to sort + // by return value or modifiers or modifiers or throws-lists since methods can't be overloaded + // by just those attributes + val p1 = o1.parameters() + val p2 = o2.parameters() + val p1n = p1.size + val p2n = p2.size + for (i in 0 until minOf(p1n, p2n)) { + val compareTypes = + p1[i].type().toTypeString().compareTo(p2[i].type().toTypeString(), ignoreCase = true) + if (compareTypes != 0) { + return compareTypes + } + // (Don't compare names; they're not part of the signatures) + } + return p1n.compareTo(p2n) + } + + return name1.compareTo(name2) + } + + val comparator: Comparator<MethodItem> = Comparator { o1, o2 -> compareMethods(o1, o2) } + val sourceOrderComparator: Comparator<MethodItem> = Comparator { o1, o2 -> + val delta = o1.sortingRank - o2.sortingRank + if (delta == 0) { + // Within a source file all the items will have unique sorting ranks, but since + // we copy methods in from hidden super classes it's possible for ranks to clash, + // and in that case we'll revert to a signature based comparison + comparator.compare(o1, o2) + } else { + delta + } + } + + /** Gets the primary super method from a given method */ + fun getPrimarySuperMethod(method: MethodItem): MethodItem? { + val superMethods = method.superMethods() + return when { + superMethods.isEmpty() -> null + superMethods.size > 1 -> { + // Prefer default methods (or super class method bodies) + superMethods + .filter { it.modifiers.isDefault() || it.containingClass().isClass() } + .forEach { return it } + superMethods[0] + } + else -> superMethods[0] + } + } + + fun sameSignature(method: MethodItem, superMethod: MethodItem, compareRawTypes: Boolean = false): Boolean { + // If the return types differ, override it (e.g. parent implements clone(), + // subclass overrides with more specific return type) + if (method.returnType() != superMethod.returnType()) { + return false + } + + // IntentService#onStart - is it here because they vary in deprecation status? + if (method.deprecated != superMethod.deprecated) { + return false + } + + // Compare modifier lists; note that here we need to + // skip modifiers that don't apply in compat mode if set + if (!method.modifiers.equivalentTo(superMethod.modifiers)) { + return false + } + + val parameterList1 = method.parameters() + val parameterList2 = superMethod.parameters() + + if (parameterList1.size != parameterList2.size) { + return false + } + + assert(parameterList1.size == parameterList2.size) + for (i in 0 until parameterList1.size) { + val p1 = parameterList1[i] + val p2 = parameterList2[i] + val pt1 = p1.type() + val pt2 = p2.type() + + if (compareRawTypes) { + if (pt1.toErasedTypeString() != pt2.toErasedTypeString()) { + return false + } + + } else { + if (pt1 != pt2) { + return false + } + } + + // TODO: Compare annotations to see for example whether + // you've refined the nullness policy; if so, that should be included + } + + // Also compare throws lists + val throwsList12 = method.throwsTypes() + val throwsList2 = superMethod.throwsTypes() + + if (throwsList12.size != throwsList2.size) { + return false + } + + assert(throwsList12.size == throwsList2.size) + for (i in 0 until throwsList12.size) { + val p1 = throwsList12[i] + val p2 = throwsList2[i] + val pt1 = p1.qualifiedName() + val pt2 = p2.qualifiedName() + if (pt1 != pt2) { // assumes throws lists are sorted! + return false + } + } + + return true + } + } + + fun formatParameters(): String? { + // TODO: Generalize, allow callers to control whether to include annotations, whether to erase types, + // whether to include names, etc + if (parameters().isEmpty()) { + return "" + } + val sb = StringBuilder() + for (parameter in parameters()) { + if (!sb.isEmpty()) { + sb.append(", ") + } + sb.append(parameter.type().toTypeString()) + } + + return sb.toString() + } + + override fun requiresNullnessInfo(): Boolean { + if (isConstructor()) { + return false + } else if (returnType()?.primitive != true) { + return true + } + for (parameter in parameters()) { + if (!parameter.type().primitive) { + return true + } + } + return false + } + + override fun hasNullnessInfo(): Boolean { + if (!isConstructor() && returnType()?.primitive != true) { + if (!modifiers.hasNullnessInfo()) { + return false + } + } + + @Suppress("LoopToCallChain") // The quickfix is wrong! (covered by AnnotationStatisticsTest) + for (parameter in parameters()) { + if (!parameter.hasNullnessInfo()) { + return false + } + } + + return true + } + + fun isImplicitConstructor(): Boolean { + return isConstructor() && modifiers.isPublic() && parameters().isEmpty() + } + + /** + * Returns true if this method is a signature match for the given method (e.g. can + * be overriding). This checks that the name and parameter lists match, but ignores + * differences in parameter names, return value types and throws list types. + */ + fun matches(other: MethodItem): Boolean { + if (this === other) return true + + if (name() != other.name()) { + return false + } + + val parameters1 = parameters() + val parameters2 = other.parameters() + + if (parameters1.size != parameters2.size) { + return false + } + + for (i in 0 until parameters1.size) { + val parameter1 = parameters1[i] + val parameter2 = parameters2[i] + val type1 = parameter1.type().toErasedTypeString() + val type2 = parameter2.type().toErasedTypeString() + if (type1 != type2) { + return false + } + } + return true + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/ModifierList.kt b/src/main/java/com/android/tools/metalava/model/ModifierList.kt new file mode 100644 index 0000000..5307ebe --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.Options +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.options +import java.io.Writer + +interface ModifierList { + val codebase: Codebase + fun annotations(): List<AnnotationItem> + + fun owner(): Item + fun isPublic(): Boolean + fun isProtected(): Boolean + fun isPrivate(): Boolean + fun isStatic(): Boolean + fun isAbstract(): Boolean + fun isFinal(): Boolean + fun isNative(): Boolean + fun isSynchronized(): Boolean + fun isStrictFp(): Boolean + fun isTransient(): Boolean + fun isVolatile(): Boolean + fun isDefault(): Boolean + + fun isEmpty(): Boolean + + fun isPackagePrivate() = !(isPublic() || isProtected() || isPrivate()) + + // Rename? It's not a full equality, it's whether an override's modifier set is significant + fun equivalentTo(other: ModifierList): Boolean { + if (isPublic() != other.isPublic()) return false + if (isProtected() != other.isProtected()) return false + if (isPrivate() != other.isPrivate()) return false + + if (isStatic() != other.isStatic()) return false + if (isAbstract() != other.isAbstract()) return false + if (isFinal() != other.isFinal()) return false + if (!compatibility.skipNativeModifier && isNative() != other.isNative()) return false + if (isSynchronized() != other.isSynchronized()) return false + if (!compatibility.skipStrictFpModifier && isStrictFp() != other.isStrictFp()) return false + if (isTransient() != other.isTransient()) return false + if (isVolatile() != other.isVolatile()) return false + + // Default does not require an override to "remove" it + //if (isDefault() != other.isDefault()) return false + + return true + } + + /** Returns true if this modifier list contains any nullness information */ + fun hasNullnessInfo(): Boolean { + return annotations().any { it.isNonNull() || it.isNullable() } + } + + /** + * Returns true if this modifier list contains any annotations explicitly passed in + * via [Options.showAnnotations] + */ + fun hasShowAnnotation(): Boolean { + if (options.showAnnotations.isEmpty()) { + return false + } + return annotations().any { + options.showAnnotations.contains(it.qualifiedName()) + } + } + + /** + * Returns true if this modifier list contains any annotations explicitly passed in + * via [Options.hideAnnotations] + */ + fun hasHideAnnotations(): Boolean { + if (options.hideAnnotations.isEmpty()) { + return false + } + return annotations().any { + options.hideAnnotations.contains(it.qualifiedName()) + } + } + + /** Returns true if this modifier list contains the given annotation */ + fun isAnnotatedWith(qualifiedName: String): Boolean { + return findAnnotation(qualifiedName) != null + } + + /** Returns the annotation of the given qualified name if found in this modifier list */ + fun findAnnotation(qualifiedName: String): AnnotationItem? { + val mappedName = AnnotationItem.mapName(codebase, qualifiedName) + return annotations().firstOrNull { + mappedName == it.qualifiedName() + } + } + + companion object { + fun write( + writer: Writer, + modifiers: ModifierList, + item: Item, + // TODO: "deprecated" isn't a modifier; clarify method name + includeDeprecated: Boolean = false, + includeAnnotations: Boolean = true, + skipNullnessAnnotations: Boolean = false, + omitCommonPackages: Boolean = false, + removeAbstract: Boolean = false, + removeFinal: Boolean = false, + addPublic: Boolean = false + ) { + + val list = if (removeAbstract || removeFinal || addPublic) { + class AbstractFiltering : ModifierList by modifiers { + override fun isAbstract(): Boolean { + return if (removeAbstract) false else modifiers.isAbstract() + } + + override fun isFinal(): Boolean { + return if (removeFinal) false else modifiers.isFinal() + } + + override fun isPublic(): Boolean { + return if (addPublic) true else modifiers.isPublic() + } + } + AbstractFiltering() + } else { + modifiers + } + + if (includeAnnotations && list.annotations().isNotEmpty()) { + for (annotation in list.annotations()) { + if ((annotation.isNonNull() || annotation.isNullable())) { + if (skipNullnessAnnotations) { + continue + } + } else if (!annotation.isSignificant()) { + continue + } + val source = annotation.toSource() + if (omitCommonPackages) { + writer.write(AnnotationItem.shortenAnnotation(source)) + } else { + writer.write(source) + } + writer.write(" ") + } + } + + // Abstract: should appear in interfaces if in compat mode + val classItem = item as? ClassItem + val methodItem = item as? MethodItem + + // Order based on the old stubs code: TODO, use Java standard order instead? + + if (compatibility.nonstandardModifierOrder) { + when { + list.isPublic() -> writer.write("public ") + list.isProtected() -> writer.write("protected ") + list.isPrivate() -> writer.write("private ") + } + + if (list.isDefault()) { + writer.write("default ") + } + + if (list.isStatic()) { + writer.write("static ") + } + + if (list.isFinal() && + // Don't show final on parameters: that's an implementation side detail + item !is ParameterItem && + (classItem?.isEnum() != true || compatibility.finalInInterfaces) || + compatibility.forceFinalInEnumValueMethods && + methodItem?.name() == "values" && methodItem.containingClass().isEnum() + ) { + writer.write("final ") + } + + val isInterface = classItem?.isInterface() == true + || (methodItem?.containingClass()?.isInterface() == true && + !list.isDefault() && !list.isStatic()) + + if ((compatibility.abstractInInterfaces && isInterface + || list.isAbstract() && + (classItem?.isEnum() != true && + (compatibility.abstractInAnnotations || classItem?.isAnnotationType() != true))) + && (!isInterface || compatibility.abstractInInterfaces) + ) { + writer.write("abstract ") + } + + if (!compatibility.skipNativeModifier && list.isNative()) { + writer.write("native ") + } + + if (item.deprecated && includeDeprecated) { + writer.write("deprecated ") + } + + if (list.isSynchronized()) { + writer.write("synchronized ") + } + + if (!compatibility.skipStrictFpModifier && list.isStrictFp()) { + writer.write("strictfp ") + } + + if (list.isTransient()) { + writer.write("transient ") + } + + if (list.isVolatile()) { + writer.write("volatile ") + } + } else { + if (item.deprecated && includeDeprecated) { + writer.write("deprecated ") + } + + when { + list.isPublic() -> writer.write("public ") + list.isProtected() -> writer.write("protected ") + list.isPrivate() -> writer.write("private ") + } + + val isInterface = classItem?.isInterface() == true + || (methodItem?.containingClass()?.isInterface() == true && + !list.isDefault() && !list.isStatic()) + + if ((compatibility.abstractInInterfaces && isInterface + || list.isAbstract() && + (classItem?.isEnum() != true && + (compatibility.abstractInAnnotations || classItem?.isAnnotationType() != true))) + && (!isInterface || compatibility.abstractInInterfaces) + ) { + writer.write("abstract ") + } + + if (list.isDefault() && item !is ParameterItem) { + writer.write("default ") + } + + if (list.isStatic()) { + writer.write("static ") + } + + if (list.isFinal() && + // Don't show final on parameters: that's an implementation side detail + item !is ParameterItem && + (classItem?.isEnum() != true || compatibility.finalInInterfaces) + ) { + writer.write("final ") + } + + if (list.isTransient()) { + writer.write("transient ") + } + + if (list.isVolatile()) { + writer.write("volatile ") + } + + if (list.isSynchronized()) { + writer.write("synchronized ") + } + + if (!compatibility.skipNativeModifier && list.isNative()) { + writer.write("native ") + } + + if (!compatibility.skipStrictFpModifier && list.isStrictFp()) { + writer.write("strictfp ") + } + } + } + } +} + diff --git a/src/main/java/com/android/tools/metalava/model/MutableModifierList.kt b/src/main/java/com/android/tools/metalava/model/MutableModifierList.kt new file mode 100644 index 0000000..feeb615 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/MutableModifierList.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 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 + +interface MutableModifierList : ModifierList { + fun setPublic(public: Boolean) + fun setProtected(protected: Boolean) + fun setPrivate(private: Boolean) + fun setStatic(static: Boolean) + fun setAbstract(abstract: Boolean) + fun setFinal(final: Boolean) + fun setNative(native: Boolean) + fun setSynchronized(synchronized: Boolean) + fun setStrictFp(strictfp: Boolean) + fun setTransient(transient: Boolean) + fun setVolatile(volatile: Boolean) + fun setDefault(default: Boolean) + + fun addAnnotation(annotation: AnnotationItem) + fun removeAnnotation(annotation: AnnotationItem) + fun clearAnnotations(annotation: AnnotationItem) + + fun setPackagePrivate(private: Boolean) { + setPublic(false) + setProtected(false) + setPrivate(false) + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/PackageItem.kt b/src/main/java/com/android/tools/metalava/model/PackageItem.kt new file mode 100644 index 0000000..ed6e31c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/PackageItem.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ApiVisitor +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor +import com.android.tools.metalava.tick + +interface PackageItem : Item { + /** The qualified name of this package */ + fun qualifiedName(): String + + /** All top level classes in this package */ + fun topLevelClasses(): Sequence<ClassItem> + + /** All top level classes **and inner classes** in this package */ + fun allClasses(): Sequence<ClassItem> { + return topLevelClasses().asSequence().flatMap { it.allClasses() } + } + + val isDefault get() = qualifiedName().isEmpty() + + override fun parent(): PackageItem? = if (qualifiedName().isEmpty()) null else containingPackage() + + fun containingPackage(): PackageItem? { + val name = qualifiedName() + val lastDot = name.lastIndexOf('.') + return if (lastDot != -1) { + codebase.findPackage(name.substring(0, lastDot)) + } else { + null + } + } + + /** Whether this package is empty */ + fun empty() = topLevelClasses().none() + + override fun accept(visitor: ItemVisitor) { + if (visitor.skipEmptyPackages && empty()) { + return + } + + if (visitor is ApiVisitor) { + if (!emit) { + return + } + + // For the API visitor packages are visited lazily; only when we encounter + // an unfiltered item within the class + topLevelClasses() + .asSequence() + .sortedWith(ClassItem.classNameSorter()) + .forEach { + tick() + it.accept(visitor) + } + + if (visitor.visitingPackage) { + visitor.visitingPackage = false + visitor.afterVisitPackage(this) + visitor.afterVisitItem(this) + } + + return + } + + + if (visitor.skip(this)) { + return + } + + visitor.visitItem(this) + visitor.visitPackage(this) + + for (cls in topLevelClasses()) { + cls.accept(visitor) + } + + visitor.afterVisitPackage(this) + visitor.afterVisitItem(this) + } + + override fun acceptTypes(visitor: TypeVisitor) { + if (visitor.skip(this)) { + return + } + + for (unit in topLevelClasses()) { + unit.acceptTypes(visitor) + } + } + + companion object { + val comparator: Comparator<PackageItem> = Comparator { a, b -> a.qualifiedName().compareTo(b.qualifiedName()) } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/PackageList.kt b/src/main/java/com/android/tools/metalava/model/PackageList.kt new file mode 100644 index 0000000..58ebf3c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/PackageList.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor + +class PackageList(val packages: List<PackageItem>) { + fun accept(visitor: ItemVisitor) { + packages.forEach { + it.accept(visitor) + } + } + + fun acceptTypes(visitor: TypeVisitor) { + packages.forEach { + it.acceptTypes(visitor) + } + } + + /** All top level classes in all packages */ + fun allTopLevelClasses(): Sequence<ClassItem> { + return packages.asSequence().flatMap { it.topLevelClasses() } + } + + /** All top level classes **and inner classes** in all packages */ + fun allClasses(): Sequence<ClassItem> { + return packages.asSequence().flatMap { it.allClasses() } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/ParameterItem.kt b/src/main/java/com/android/tools/metalava/model/ParameterItem.kt new file mode 100644 index 0000000..6aa6e22 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ParameterItem.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.android.tools.metalava.model.visitors.TypeVisitor + +interface ParameterItem : Item { + /** The name of this field */ + fun name(): String + + /** The type of this field */ + fun type(): TypeItem + + /** The containing method */ + fun containingMethod(): MethodItem + + /** Index of this parameter in the parameter list (0-based) */ + val parameterIndex: Int + + /** + * The public name of this parameter. In Kotlin, names are part of the + * public API; in Java they are not. In Java, you can annotate a + * parameter with {@literal @ParameterName("foo")} to name the parameter + * something (potentially different from the actual code parameter name). + */ + fun publicName(): String? + + override fun parent(): MethodItem? = containingMethod() + + override fun accept(visitor: ItemVisitor) { + if (visitor.skip(this)) { + return + } + + visitor.visitItem(this) + visitor.visitParameter(this) + + visitor.afterVisitParameter(this) + visitor.afterVisitItem(this) + } + + override fun acceptTypes(visitor: TypeVisitor) { + if (visitor.skip(this)) { + return + } + + val type = type() + visitor.visitType(type, this) + visitor.afterVisitType(type, this) + } + + override fun requiresNullnessInfo(): Boolean { + return !type().primitive + } + + override fun hasNullnessInfo(): Boolean { + if (!requiresNullnessInfo()) { + return true + } + + return modifiers.hasNullnessInfo() + } + + // TODO: modifier list +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/src/main/java/com/android/tools/metalava/model/TypeItem.kt new file mode 100644 index 0000000..937b5ec --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.lint.detector.api.ClassContext +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.options +import java.util.function.Predicate + +/** Represents a type */ +interface TypeItem { + /** + * Generates a string for this type. + * + * For a type like this: @Nullable java.util.List<@NonNull java.lang.String>, + * [outerAnnotations] controls whether the top level annotation like @Nullable + * is included, [innerAnnotations] controls whether annotations like @NonNull + * are included, and [erased] controls whether we return the string for + * the raw type, e.g. just "java.util.List" + * + * (The combination [outerAnnotations] = true and [innerAnnotations] = false + * is not allowed.) + */ + fun toTypeString( + outerAnnotations: Boolean = false, + innerAnnotations: Boolean = outerAnnotations, + erased: Boolean = false + ): String + + /** Alias for [toTypeString] with erased=true */ + fun toErasedTypeString(): String + + /** Returns the internal name of the type, as seen in bytecode */ + fun internalName(): String { + // Default implementation; PSI subclass is more accurate + return toSlashFormat(toErasedTypeString()) + } + + fun asClass(): ClassItem? + + fun toSimpleType(): String { + return toTypeString().replace("java.lang.", "") + } + + val primitive: Boolean + + fun typeArgumentClasses(): List<ClassItem> + + fun convertType(from: ClassItem, to: ClassItem): TypeItem { + val map = from.mapTypeVariables(to) + if (!map.isEmpty()) { + return convertType(map) + } + + return this + } + + fun convertType(replacementMap: Map<String, String>?, owner: Item? = null): TypeItem + + fun convertTypeString(replacementMap: Map<String, String>?): String { + return convertTypeString(toTypeString(outerAnnotations = true, innerAnnotations = true), replacementMap) + } + + fun isJavaLangObject(): Boolean { + return toTypeString() == "java.lang.Object" + } + + fun defaultValue(): Any? { + return when (toTypeString()) { + "boolean" -> false + "byte" -> 0.toByte() + "char" -> '\u0000' + "short" -> 0.toShort() + "int" -> 0 + "long" -> 0L + "float" -> 0f + "double" -> 0.0 + else -> null + } + } + + /** Returns true if this type references a type not matched by the given predicate */ + fun referencesExcludedType(filter: Predicate<Item>): Boolean { + if (primitive) { + return false + } + + for (item in typeArgumentClasses()) { + if (!filter.test(item)) { + return true + } + } + + return false + } + + fun defaultValueString(): String = defaultValue()?.toString() ?: "null" + + fun hasTypeArguments(): Boolean = toTypeString().contains("<") + + 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." + + fun shortenTypes(type: String): String { + if (options.omitCommonPackages && + (type.contains("java.lang.") || + type.contains("@android.support.annotation.")) + ) { + var cleaned = type + if (options.omitCommonPackages) { + if (cleaned.contains(ANDROID_SUPPORT_ANNOTATION_PREFIX)) { + cleaned = cleaned.replace(ANDROID_SUPPORT_ANNOTATION_PREFIX, "@") + } + } + + // 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) + while (index != -1) { + val start = index + JAVA_LANG_PREFIX.length + val end = cleaned.length + for (index2 in start..end) { + if (index2 == end) { + val suffix = cleaned.substring(start) + cleaned = if (index == 0) { + suffix + } else { + cleaned.substring(0, index) + suffix + } + break + } + val c = cleaned[index2] + if (c == '.') { + break + } else if (!Character.isJavaIdentifierPart(c)) { + val suffix = cleaned.substring(start) + cleaned = if (index == 0) { + suffix + } else { + cleaned.substring(0, index) + suffix + } + break + } + } + + index = cleaned.indexOf(JAVA_LANG_PREFIX, start) + } + + return cleaned + } + + return type + } + + fun formatType(type: String?): String { + if (type == null) { + return "" + } + + var cleaned = type + + if (compatibility.spacesAfterCommas && cleaned.indexOf(',') != -1) { + // The compat files have spaces after commas where we normally don't + cleaned = cleaned.replace(",", ", ").replace(", ", ", ") + } + + cleaned = cleanupGenerics(cleaned) + return cleaned + } + + fun cleanupGenerics(signature: String): String { + // <T extends java.lang.Object> is the same as <T> + // but NOT for <T extends Object & java.lang.Comparable> -- you can't + // shorten this to <T & java.lang.Comparable + //return type.replace(" extends java.lang.Object", "") + return signature.replace(" extends java.lang.Object>", ">") + + } + + val comparator: Comparator<TypeItem> = Comparator { type1, type2 -> + val cls1 = type1.asClass() + val cls2 = type2.asClass() + if (cls1 != null && cls2 != null) { + ClassItem.fullNameComparator.compare(cls1, cls2) + } else { + type1.toTypeString().compareTo(type2.toTypeString()) + } + } + + fun convertTypeString(typeString: String, replacementMap: Map<String, String>?): String { + var string = typeString + if (replacementMap != null && replacementMap.isNotEmpty()) { + // This is a moved method (typically an implementation of an interface + // method provided in a hidden superclass), with generics signatures. + // We need to rewrite the generics variables in case they differ + // between the classes. + if (!replacementMap.isEmpty()) { + replacementMap.forEach { from, to -> + // We can't just replace one string at a time: + // what if I have a map of {"A"->"B", "B"->"C"} and I tried to convert A,B,C? + // If I do the replacements one letter at a time I end up with C,C,C; if I do the substitutions + // simultaneously I get B,C,C. Therefore, we insert "___" as a magical prefix to prevent + // scenarios like this, and then we'll drop them afterwards. + string = string.replace(Regex(pattern = """\b$from\b"""), replacement = "___$to") + } + } + string = string.replace("___", "") + return string + } else { + return string + } + } + + // Copied from doclava1 + fun toSlashFormat(typeName: String): String { + var name = typeName + var dimension = "" + while (name.endsWith("[]")) { + dimension += "[" + name = name.substring(0, name.length - 2) + } + + val base: String + if (name == "void") { + base = "V" + } else if (name == "byte") { + base = "B" + } else if (name == "boolean") { + base = "Z" + } else if (name == "char") { + base = "C" + } else if (name == "short") { + base = "S" + } else if (name == "int") { + base = "I" + } else if (name == "long") { + base = "L" + } else if (name == "float") { + base = "F" + } else if (name == "double") { + base = "D" + } else { + base = "L" + ClassContext.getInternalName(name) + ";" + } + + return dimension + base + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt b/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt new file mode 100644 index 0000000..76710cd --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 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.intellij.psi.PsiClass + +enum class ClassType { + INTERFACE, + ENUM, + ANNOTATION_TYPE, + CLASS; + + companion object { + fun getClassType(psiClass: PsiClass): ClassType { + return when { + psiClass.isAnnotationType -> ANNOTATION_TYPE + psiClass.isInterface -> INTERFACE + psiClass.isEnum -> ENUM + else -> CLASS + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/Javadoc.kt b/src/main/java/com/android/tools/metalava/model/psi/Javadoc.kt new file mode 100644 index 0000000..31620b5 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/Javadoc.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2017 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.intellij.psi.JavaDocTokenType +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.javadoc.PsiDocToken + +/* + * Various utilities for merging comments into existing javadoc sections. + * + * TODO: Handle KDoc + */ + +/** + * Merges the given [newText] into the existing documentation block [existingDoc] + * (which should be a full documentation node, including the surrounding comment + * start and end tokens.) + * + * If the [tagSection] is null, add the comment to the initial text block + * of the description. Otherwise if it is "@return", add the comment + * to the return value. Otherwise the [tagSection] is taken to be the + * parameter name, and the comment added as parameter documentation + * for the given parameter. + */ +fun mergeDocumentation( + existingDoc: String, + psiElement: PsiElement, + newText: String, + tagSection: String?, + append: Boolean +): String { + + if (existingDoc.isBlank()) { + // There's no existing comment: Create a new one. This is easy. + val content = when { + tagSection == "@return" -> "@return $newText" + tagSection?.startsWith("@") ?: false -> "$tagSection $newText" + tagSection != null -> "@param $tagSection $newText" + else -> newText + } + + // TODO: Handle prefixing "*" on lines, if already done in the document? + return if (newText.contains('\n')) { + "/** $content */" + } else { + return insertInto("/**\n */", content, 3) + } + } + + val doc = trimDocIndent(existingDoc) + + // We'll use the PSI Javadoc support to parse the documentation + // to help us scan the tokens in the documentation, such that + // we don't have to search for raw substrings like "@return" which + // can incorrectly find matches in escaped code snippets etc. + val factory = JavaPsiFacade.getElementFactory(psiElement.project) + ?: error("Invalid tool configuration; did not find JavaPsiFacade factory") + val docComment = factory.createDocCommentFromText(doc) + + if (tagSection == "@return") { + // Add in return value + val returnTag = docComment.findTagByName("return") + if (returnTag == null) { + // Find last tag + val lastTag = findLastTag(docComment) + val offset = if (lastTag != null) { + findTagEnd(lastTag) + } else { + doc.length - 2 + } + return insertInto(doc, "@return $newText", offset) + } else { + // Add text to the existing @return tag + val offset = if (append) + findTagEnd(returnTag) + else + returnTag.textRange.startOffset + returnTag.name.length + 1 + return insertInto(doc, newText, offset) + } + } else if (tagSection != null) { + val parameter = if (tagSection.startsWith("@")) + docComment.findTagByName(tagSection.substring(1)) + else + findParamTag(docComment, tagSection) + if (parameter == null) { + // Add new parameter or tag + // TODO: Decide whether to place it alphabetically or place it by parameter order + // in the signature. Arguably I should follow the convention already present in the + // doc, if any + // For now just appending to the last tag before the return tag (if any). + // This actually works out well in practice where arguments are generally all documented + // or all not documented; when none of the arguments are documented these end up appending + // exactly in the right parameter order! + val returnTag = docComment.findTagByName("return") + val anchor = returnTag ?: findLastTag(docComment) + val offset = when { + returnTag != null -> returnTag.textRange.startOffset + anchor != null -> findTagEnd(anchor) + else -> doc.length - 2 // "*/ + } + val tagName = if (tagSection.startsWith("@")) tagSection else "@param $tagSection" + return insertInto(doc, "$tagName $newText", offset) + } else { + // Add to existing tag/parameter + val offset = if (append) + findTagEnd(parameter) + else + parameter.textRange.startOffset + parameter.name.length + 1 + return insertInto(doc, newText, offset) + } + } else { + // Add to the main text section of the comment. + val firstTag = findFirstTag(docComment) + val startOffset = + if (!append) { + 4 // "/** ".length + } else if (firstTag != null) { + firstTag.textRange.startOffset + } else { + doc.length - 2 // -2: end marker */ + } + return insertInto(doc, newText, startOffset) + } +} + +fun findParamTag(docComment: PsiDocComment, paramName: String): PsiDocTag? { + return docComment.findTagsByName("param").firstOrNull { it.valueElement?.text == paramName } +} + +fun findFirstTag(docComment: PsiDocComment): PsiDocTag? { + return docComment.tags.asSequence().minBy { it.textRange.startOffset } +} + +fun findLastTag(docComment: PsiDocComment): PsiDocTag? { + return docComment.tags.asSequence().maxBy { it.textRange.startOffset } +} + +fun findTagEnd(tag: PsiDocTag): Int { + var curr: PsiElement? = tag.nextSibling + while (curr != null) { + if (curr is PsiDocToken && curr.tokenType == JavaDocTokenType.DOC_COMMENT_END) { + return curr.textRange.startOffset + } else if (curr is PsiDocTag) { + return curr.textRange.startOffset + } + + curr = curr.nextSibling + } + + return tag.textRange.endOffset +} + +fun trimDocIndent(existingDoc: String): String { + val index = existingDoc.indexOf('\n') + if (index == -1) { + return existingDoc + } + + return existingDoc.substring(0, index + 1) + + existingDoc.substring(index + 1).trimIndent().split('\n').joinToString(separator = "\n") { + if (!it.startsWith(" ")) { + " ${it.trimEnd()}" + } else { + it.trimEnd() + } + } +} + +fun insertInto(existingDoc: String, newText: String, initialOffset: Int): String { + // TODO: Insert "." between existing documentation and new documentation, if necessary. + + val offset = if (initialOffset > 4 && existingDoc.regionMatches(initialOffset - 4, "\n * ", 0, 4, false)) { + initialOffset - 4 + } else { + initialOffset + } + val index = existingDoc.indexOf('\n') + val prefixWithStar = index == -1 || existingDoc[index + 1] == '*' || + existingDoc[index + 1] == ' ' && existingDoc[index + 2] == '*' + + val prefix = existingDoc.substring(0, offset) + val suffix = existingDoc.substring(offset) + val startSeparator = "\n" + val endSeparator = + if (suffix.startsWith("\n") || suffix.startsWith(" \n")) "" else if (suffix == "*/") "\n" else if (prefixWithStar) "\n * " else "\n" + + val middle = if (prefixWithStar) { + startSeparator + newText.split('\n').joinToString(separator = "\n") { " * $it" } + + endSeparator + } else { + "$startSeparator$newText$endSeparator" + } + + // Going from single-line to multi-line? + return if (existingDoc.indexOf('\n') == -1 && existingDoc.startsWith("/** ")) { + prefix.substring(0, 3) + "\n *" + prefix.substring(3) + middle + + if (suffix == "*/") " */" else suffix + } else { + prefix + middle + suffix + } +} diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt new file mode 100644 index 0000000..b8ebb45 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2017 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.SdkConstants.ATTR_VALUE +import com.android.tools.lint.detector.api.ConstantEvaluator +import com.android.tools.metalava.XmlBackedAnnotationItem +import com.android.tools.metalava.model.AnnotationArrayAttributeValue +import com.android.tools.metalava.model.AnnotationAttribute +import com.android.tools.metalava.model.AnnotationAttributeValue +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.AnnotationSingleAttributeValue +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.canonicalizeFloatingPointString +import com.android.tools.metalava.model.javaEscapeString +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiAnnotationMemberValue +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiBinaryExpression +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiField +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReference +import com.intellij.psi.impl.JavaConstantExpressionEvaluator +import org.jetbrains.kotlin.asJava.elements.KtLightNullabilityAnnotation + +class PsiAnnotationItem private constructor( + override val codebase: PsiBasedCodebase, + val psiAnnotation: PsiAnnotation +) : AnnotationItem { + private var attributes: List<AnnotationAttribute>? = null + + override fun toString(): String = toSource() + + override fun toSource(): String { + val qualifiedName = qualifiedName() ?: return "" + + val attributes = psiAnnotation.parameterList.attributes + if (attributes.isEmpty()) { + return "@" + qualifiedName + } + + val sb = StringBuilder(30) + sb.append("@") + sb.append(qualifiedName) + sb.append("(") + if (attributes.size == 1 && (attributes[0].name == null || attributes[0].name == ATTR_VALUE)) { + // Special case: omit "value" if it's the only attribute + appendValue(sb, attributes[0].value) + } else { + var first = true + for (attribute in attributes) { + if (first) { + first = false + } else { + sb.append(", ") + } + sb.append(attribute.name ?: ATTR_VALUE) + sb.append('=') + appendValue(sb, attribute.value) + } + } + sb.append(")") + + return sb.toString() + } + + override fun resolve(): ClassItem? { + return codebase.findClass(psiAnnotation.qualifiedName ?: return null) + } + + private fun appendValue(sb: StringBuilder, value: PsiAnnotationMemberValue?) { + // Compute annotation string -- we don't just use value.text here + // because that may not use fully qualified names, e.g. the source may say + // @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + // and we want to compute + // @android.support.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + when (value) { + null -> sb.append("null") + is PsiLiteral -> sb.append(literalToString(value.value)) + is PsiReference -> { + val resolved = value.resolve() + when (resolved) { + is PsiField -> { + val containing = resolved.containingClass + if (containing != null) { + // If it's a field reference, see if it looks like the field is hidden; if + // so, inline the value + val cls = codebase.findOrCreateClass(containing) + val initializer = resolved.initializer + if (initializer != null) { + val fieldItem = cls.findField(resolved.name) + if (fieldItem == null || fieldItem.isHiddenOrRemoved()) { + // Use the literal value instead + val source = getConstantSource(initializer) + if (source != null) { + sb.append(source) + return + } + } + } + containing.qualifiedName?.let { + sb.append(it).append('.') + } + } + + sb.append(resolved.name) + } + is PsiClass -> resolved.qualifiedName?.let { sb.append(it) } + else -> { + sb.append(value.text) + } + } + } + is PsiBinaryExpression -> { + appendValue(sb, value.lOperand) + sb.append(' ') + sb.append(value.operationSign.text) + sb.append(' ') + appendValue(sb, value.rOperand) + } + is PsiArrayInitializerMemberValue -> { + sb.append('{') + var first = true + for (initializer in value.initializers) { + if (first) { + first = false + } else { + sb.append(", ") + } + appendValue(sb, initializer) + } + sb.append('}') + } + else -> { + if (value is PsiExpression) { + val source = getConstantSource(value) + if (source != null) { + sb.append(source) + return + } + } + sb.append(value.text) + } + } + } + + override fun isNonNull(): Boolean { + if (psiAnnotation is KtLightNullabilityAnnotation && + psiAnnotation.qualifiedName == "" + ) { + // Hack/workaround: some UAST annotation nodes do not provide qualified name :=( + return true + } + return super.isNonNull() + } + + private fun getConstantSource(value: PsiExpression): String? { + val constant = JavaConstantExpressionEvaluator.computeConstantExpression(value, false) + return when (constant) { + is Int -> "0x${Integer.toHexString(constant)}" + is String -> "\"${javaEscapeString(constant)}\"" + is Long -> "${constant}L" + is Boolean -> constant.toString() + is Byte -> Integer.toHexString(constant.toInt()) + is Short -> Integer.toHexString(constant.toInt()) + is Float -> { + when (constant) { + Float.POSITIVE_INFINITY -> "Float.POSITIVE_INFINITY" + Float.NEGATIVE_INFINITY -> "Float.NEGATIVE_INFINITY" + Float.NaN -> "Float.NaN" + else -> { + "${canonicalizeFloatingPointString(constant.toString())}F" + } + } + } + is Double -> { + when (constant) { + Double.POSITIVE_INFINITY -> "Double.POSITIVE_INFINITY" + Double.NEGATIVE_INFINITY -> "Double.NEGATIVE_INFINITY" + Double.NaN -> "Double.NaN" + else -> { + canonicalizeFloatingPointString(constant.toString()) + } + } + } + is Char -> { + "'${javaEscapeString(constant.toString())}'" + } + else -> { + null + } + } + } + + private fun literalToString(value: Any?): String { + if (value == null) { + return "null" + } + + when (value) { + is Int -> { + return value.toString() + } + is String -> { + return "\"${javaEscapeString(value)}\"" + } + is Long -> { + return value.toString() + "L" + } + is Boolean -> { + return value.toString() + } + is Byte -> { + return Integer.toHexString(value.toInt()) + } + is Short -> { + return Integer.toHexString(value.toInt()) + } + is Float -> { + return when (value) { + Float.POSITIVE_INFINITY -> "(1.0f/0.0f)" + Float.NEGATIVE_INFINITY -> "(-1.0f/0.0f)" + Float.NaN -> "(0.0f/0.0f)" + else -> { + canonicalizeFloatingPointString(value.toString()) + "f" + } + } + } + is Double -> { + return when (value) { + Double.POSITIVE_INFINITY -> "(1.0/0.0)" + Double.NEGATIVE_INFINITY -> "(-1.0/0.0)" + Double.NaN -> "(0.0/0.0)" + else -> { + canonicalizeFloatingPointString(value.toString()) + } + } + } + is Char -> { + return String.format("'%s'", javaEscapeString(value.toString())) + } + } + + return value.toString() + } + + override fun qualifiedName() = AnnotationItem.mapName(codebase, psiAnnotation.qualifiedName) + + override fun attributes(): List<AnnotationAttribute> { + if (attributes == null) { + val psiAttributes = psiAnnotation.parameterList.attributes + attributes = if (psiAttributes.isEmpty()) { + emptyList() + } else { + val list = mutableListOf<AnnotationAttribute>() + for (parameter in psiAttributes) { + list.add( + PsiAnnotationAttribute( + codebase, + parameter.name ?: ATTR_VALUE, parameter.value ?: continue + ) + ) + } + list + } + } + + return attributes!! + } + + companion object { + fun create(codebase: PsiBasedCodebase, psiAnnotation: PsiAnnotation): PsiAnnotationItem { + return PsiAnnotationItem(codebase, psiAnnotation) + } + + fun create(codebase: PsiBasedCodebase, original: PsiAnnotationItem): PsiAnnotationItem { + return PsiAnnotationItem(codebase, original.psiAnnotation) + } + + // TODO: Inline this such that instead of constructing XmlBackedAnnotationItem + // and then producing source and parsing it, produce source directly + fun create( + codebase: Codebase, xmlAnnotation: XmlBackedAnnotationItem, + context: Item? = null + ): PsiAnnotationItem { + if (codebase is PsiBasedCodebase) { + return codebase.createAnnotation(xmlAnnotation.toSource(), context) + } else { + codebase.unsupported("Converting to PSI annotation requires PSI codebase") + } + } + } +} + +class PsiAnnotationAttribute( + codebase: PsiBasedCodebase, + override val name: String, + psiValue: PsiAnnotationMemberValue +) : AnnotationAttribute { + override val value: AnnotationAttributeValue = PsiAnnotationValue.create( + codebase, psiValue + ) +} + +abstract class PsiAnnotationValue : AnnotationAttributeValue { + companion object { + fun create(codebase: PsiBasedCodebase, value: PsiAnnotationMemberValue): PsiAnnotationValue { + return if (value is PsiArrayInitializerMemberValue) { + PsiAnnotationArrayAttributeValue(codebase, value) + } else { + PsiAnnotationSingleAttributeValue(codebase, value) + } + } + } + + override fun toString(): String = toSource() +} + +class PsiAnnotationSingleAttributeValue( + private val codebase: PsiBasedCodebase, + private val psiValue: PsiAnnotationMemberValue +) : PsiAnnotationValue(), AnnotationSingleAttributeValue { + override val valueSource: String = psiValue.text + override val value: Any? + get() { + if (psiValue is PsiLiteral) { + return psiValue.value + } + + val value = ConstantEvaluator.evaluate(null, psiValue) + if (value != null) { + return value + } + + return psiValue.text + } + + override fun value(): Any? = value + + override fun toSource(): String = psiValue.text + + override fun resolve(): Item? { + if (psiValue is PsiReference) { + val resolved = psiValue.resolve() + when (resolved) { + is PsiField -> return codebase.findField(resolved) + is PsiClass -> return codebase.findOrCreateClass(resolved) + is PsiMethod -> return codebase.findMethod(resolved) + } + } + return null + } +} + +class PsiAnnotationArrayAttributeValue(codebase: PsiBasedCodebase, private val value: PsiArrayInitializerMemberValue) : + PsiAnnotationValue(), AnnotationArrayAttributeValue { + override val values = value.initializers.map { + create(codebase, it) + }.toList() + + override fun toSource(): String = value.text +} 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 new file mode 100644 index 0000000..de788ff --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt @@ -0,0 +1,974 @@ +/* + * Copyright (C) 2017 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.SdkConstants +import com.android.tools.metalava.PackageDocs +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultCodebase +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.PackageList +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.options +import com.android.tools.metalava.reporter +import com.android.tools.metalava.tick +import com.google.common.collect.BiMap +import com.google.common.collect.HashBiMap +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiPackage +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameter +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil +import org.intellij.lang.annotations.Language +import org.jetbrains.uast.UFile +import org.jetbrains.uast.UastContext +import java.io.File +import java.io.IOException +import java.util.ArrayDeque +import java.util.ArrayList +import java.util.HashMap +import java.util.function.Predicate +import java.util.zip.ZipFile + +const val PACKAGE_ESTIMATE = 400 +const val CLASS_ESTIMATE = 12000 +const val METHOD_ESTIMATE = 1000 + +open class PsiBasedCodebase(override var description: String = "Unknown") : DefaultCodebase() { + lateinit var project: Project + + /** Map from class name to class item */ + protected val classMap: MutableMap<String, PsiClassItem> = HashMap(CLASS_ESTIMATE) + + /** Map from psi type to type item */ + private val typeMap: MutableMap<PsiType, TypeItem> = HashMap(400) + + /** + * Map from classes to the set of methods for each (but only for classes where we've + * called [findMethod] + */ + private lateinit var methodMap: MutableMap<PsiClassItem, MutableMap<PsiMethod, PsiMethodItem>> + + /** Map from package name to the corresponding package item */ + private lateinit var packageMap: MutableMap<String, PsiPackageItem> + + /** Map from package name to list of classes in that package */ + private lateinit var packageClasses: MutableMap<String, MutableList<PsiClassItem>> + + /** A set of packages to hide */ + private lateinit var hiddenPackages: MutableMap<String, Boolean?> + + private var initializing = false + + override fun trustedApi(): Boolean = false + + private var packageDocs: PackageDocs? = null + + private var hideClassesFromJars = true + + private lateinit var emptyPackage: PsiPackageItem + + fun initialize(project: Project, units: List<PsiFile>, packages: PackageDocs) { + initializing = true + this.units = units + packageDocs = packages + + this.project = project + // there are currently ~230 packages in the public SDK, but here we need to account for internal ones too + val hiddenPackages: MutableSet<String> = packages.hiddenPackages + val packageDocs: MutableMap<String, String> = packages.packageDocs + this.hiddenPackages = HashMap(100) + for (pkgName in hiddenPackages) { + this.hiddenPackages[pkgName] = true + } + + packageMap = HashMap(PACKAGE_ESTIMATE) + packageClasses = HashMap(PACKAGE_ESTIMATE) + packageClasses[""] = ArrayList() + this.methodMap = HashMap(METHOD_ESTIMATE) + + for (unit in units) { + tick() // show progress + + val topLevelClasses = mutableListOf<ClassItem>() + var classes = (unit as? PsiClassOwner)?.classes?.toList() ?: emptyList() + if (classes.isEmpty()) { + val uastContext = project.getComponent(UastContext::class.java) + val uFile = uastContext.convertElementWithParent(unit, UFile::class.java) as? UFile? + classes = uFile?.classes?.map { it }?.toList() ?: emptyList() + } + var packageName: String? = null + if (classes.isEmpty() && unit is PsiJavaFile) { + val packageStatement = unit.packageStatement + // Look for javadoc on the package statement; this is NOT handed to us on + // the PsiPackage! + if (packageStatement != null) { + packageName = packageStatement.packageName + val comment = PsiTreeUtil.getPrevSiblingOfType(packageStatement, PsiDocComment::class.java) + if (comment != null) { + val text = comment.text + if (text.contains("@hide")) { + hiddenPackages.add(packageName) + } + packageDocs[packageName] = text + (packageDocs[packageName] ?: "") + } + } + } else { + for (psiClass in classes) { + val classItem = createClass(psiClass) + topLevelClasses.add(classItem) + + if (packageName == null) { + packageName = getPackageName(psiClass) + } + } + } + } + + // Next construct packages + for ((pkgName, classes) in packageClasses) { + tick() // show progress + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(pkgName) + if (psiPackage == null) { + println("Could not find package $pkgName") + continue + } + + val sortedClasses = classes.toMutableList().sortedWith(ClassItem.fullNameComparator) + val packageHtml = packageDocs[pkgName] + registerPackage(psiPackage, sortedClasses, packageHtml, pkgName) + } + + initializing = false + + emptyPackage = findPackage("")!! + + // Finish initialization + val initialPackages = ArrayList(packageMap.values) + var registeredCount = packageMap.size // classes added after this point will have indices >= original + for (cls in initialPackages) { + cls.finishInitialization() + } + + // Finish initialization of any additional classes that were registered during + // the above initialization (recursively) + while (registeredCount < packageMap.size) { + val added = packageMap.values.minus(initialPackages) + registeredCount = packageMap.size + for (pkg in added) { + pkg.finishInitialization() + } + } + + // Point to "parent" packages, since doclava treats packages as nested (e.g. an @hide on + // android.foo will also apply to android.foo.bar) + addParentPackages(packageMap.values) + } + + private fun addParentPackages(packages: Collection<PsiPackageItem>) { + val missingPackages = packages.mapNotNull { + val name = it.qualifiedName() + val index = name.lastIndexOf('.') + val parent = if (index != -1) { + name.substring(0, index) + } else { + "" + } + if (packageMap.containsKey(parent)) { + // Already registered + null + } else { + parent + } + }.toSet() + + // Create PackageItems for any packages that weren't in the source + for (pkgName in missingPackages) { + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(pkgName) ?: continue + val sortedClasses = emptyList<PsiClassItem>() + val packageHtml = null + val pkg = registerPackage(psiPackage, sortedClasses, packageHtml, pkgName) + pkg.emit = false // don't expose these packages in the API signature files, stubs, etc + } + + // Connect up all the package items + for (pkg in packageMap.values) { + var name = pkg.qualifiedName() + // Find parent package; we have to loop since we don't always find a PSI package + // for intermediate elements; e.g. we may jump from java.lang straing up to the default + // package + while (name.isNotEmpty()) { + val index = name.lastIndexOf('.') + if (index != -1) { + name = name.substring(0, index) + } else { + name = "" + } + val parent = findPackage(name) ?: continue + pkg.containingPackageField = parent + break + } + } + } + + private fun registerPackage( + psiPackage: PsiPackage, + sortedClasses: List<PsiClassItem>?, + packageHtml: String?, + pkgName: String + ): PsiPackageItem { + val packageItem = PsiPackageItem.create( + this, psiPackage, + packageHtml + ) + packageMap[pkgName] = packageItem + if (isPackageHidden(pkgName)) { + packageItem.hidden = true + } + + sortedClasses?.let { packageItem.addClasses(it) } + return packageItem + } + + fun initialize(project: Project, jarFile: File) { + initializing = true + hideClassesFromJars = false + + this.project = project + + // Find all classes referenced from the class + val facade = JavaPsiFacade.getInstance(project) + val scope = GlobalSearchScope.allScope(project) + + hiddenPackages = HashMap(100) + packageMap = HashMap(PACKAGE_ESTIMATE) + packageClasses = HashMap(PACKAGE_ESTIMATE) + packageClasses[""] = ArrayList() + this.methodMap = HashMap(1000) + val packageToClasses: MutableMap<String, MutableList<PsiClassItem>> = HashMap( + PACKAGE_ESTIMATE + ) + packageToClasses[""] = ArrayList() // ensure we construct one for the default package + + val topLevelClasses = ArrayList<ClassItem>(CLASS_ESTIMATE) + + try { + ZipFile(jarFile).use({ jar -> + val enumeration = jar.entries() + while (enumeration.hasMoreElements()) { + val entry = enumeration.nextElement() + val fileName = entry.name + if (fileName.contains("$")) { + // skip inner classes + continue + } + if (fileName.endsWith(SdkConstants.DOT_CLASS)) { + val qualifiedName = fileName.removeSuffix(SdkConstants.DOT_CLASS).replace('/', '.') + if (qualifiedName.endsWith(".package-info")) { + // Ensure we register a package for this, even if empty + val packageName = qualifiedName.removeSuffix(".package-info") + var list = packageToClasses[packageName] + if (list == null) { + list = mutableListOf() + packageToClasses[packageName] = list + } + continue + } else { + val psiClass = facade.findClass(qualifiedName, scope) ?: continue + + val classItem = createClass(psiClass) + topLevelClasses.add(classItem) + + val packageName = getPackageName(psiClass) + var list = packageToClasses[packageName] + if (list == null) { + list = mutableListOf(classItem) + packageToClasses[packageName] = list + } else { + list.add(classItem) + } + } + } + } + }) + } catch (e: IOException) { + reporter.report(Errors.IO_ERROR, jarFile, e.message ?: e.toString()) + } + + // Next construct packages + for ((pkgName, packageClasses) in packageToClasses) { + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(pkgName) + if (psiPackage == null) { + println("Could not find package $pkgName") + continue + } + + packageClasses.sortWith(ClassItem.fullNameComparator) + // TODO: How do we obtain the package docs? We generally don't have them, but it *would* be + // nice if we picked up "overview.html" bundled files and added them. But since the docs + // are generally missing for all elements *anyway*, let's not bother. + val packageHtml: String? = packageDocs?.packageDocs!![pkgName] + registerPackage(psiPackage, packageClasses, packageHtml, pkgName) + } + + emptyPackage = findPackage("")!! + + initializing = false + hideClassesFromJars = true + + // Finish initialization + for (pkg in packageMap.values) { + pkg.finishInitialization() + } + + } + + fun dumpStats() { + options.stdout.println( + "INTERNAL STATS: Size of classMap=${classMap.size} and size of " + + "methodMap=${methodMap.size} and size of packageMap=${packageMap.size}, and the " + + "typemap size is ${typeMap.size}, and the packageClasses size is ${packageClasses.size} " + ) + } + + private fun registerPackageClass(packageName: String, cls: PsiClassItem) { + var list = packageClasses[packageName] + if (list == null) { + list = ArrayList() + packageClasses[packageName] = list + } + + if (isPackageHidden(packageName)) { + cls.hidden = true + } + + list.add(cls) + } + + private fun isPackageHidden(packageName: String): Boolean { + val hidden = hiddenPackages[packageName] + if (hidden == true) { + return true + } else if (hidden == null) { + // Compute for all prefixes of this package + var pkg = packageName + while (true) { + if (hiddenPackages[pkg] != null) { + hiddenPackages[packageName] = hiddenPackages[pkg] + if (hiddenPackages[pkg] == true) { + return true + } + } + val last = pkg.lastIndexOf('.') + if (last == -1 || !compatibility.inheritPackageDocs) { + hiddenPackages[packageName] = false + break + } else { + pkg = pkg.substring(0, last) + } + } + } + + return false + } + + private fun createClass(clz: PsiClass): PsiClassItem { + val classItem = PsiClassItem.create(this, clz) + + if (!initializing && options.hideClasspathClasses) { + // This class is found while we're no longer initializing all the source units: + // that means it must be found on the classpath instead. These should be treated + // as hidden; we don't want to generate code for them. + classItem.emit = false + + // Workaround: we're pulling in .aidl files from .jar files. These are + // marked @hide, but since we only see the .class files we don't know that. + if (classItem.simpleName().startsWith("I") && + classItem.isFromClassPath() && + clz.interfaces.any { it.qualifiedName == "android.os.IInterface" } + ) { + classItem.hidden = true + } + } + + if (clz is PsiTypeParameter) { + // Don't put PsiTypeParameter classes into the registry; e.g. when we're visiting + // java.util.stream.Stream<R> + // we come across "R" and would try to place it here. + classItem.containingPackage = emptyPackage + return classItem + } + val qualifiedName: String = clz.qualifiedName ?: clz.name!! + classMap[qualifiedName] = classItem + + // TODO: Cache for adjacent files! + val packageName = getPackageName(clz) + registerPackageClass(packageName, classItem) + + if (!initializing) { + classItem.emit = false + classItem.finishInitialization() + val pkgName = getPackageName(clz) + val pkg = findPackage(pkgName) + if (pkg == null) { + //val packageHtml: String? = packageDocs?.packageDocs!![pkgName] + // dynamically discovered packages should NOT be included + //val packageHtml = "/** @hide */" + val packageHtml = null + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(pkgName) + if (psiPackage != null) { + val packageItem = registerPackage(psiPackage, null, packageHtml, pkgName) + // Don't include packages from API that isn't directly included in the API + if (options.hideClasspathClasses) { + packageItem.emit = false + } + packageItem.addClass(classItem) + } + } else { + pkg.addClass(classItem) + } + } + + return classItem + } + + override fun getPackages(): PackageList { + // TODO: Sorting is probably not necessary here! + return PackageList(packageMap.values.toMutableList().sortedWith(PackageItem.comparator)) + } + + override fun size(): Int { + return packageMap.size + } + + override fun findPackage(pkgName: String): PsiPackageItem? { + return packageMap[pkgName] + } + + override fun findClass(className: String): PsiClassItem? { + return classMap[className] + } + + open fun findClass(psiClass: PsiClass): PsiClassItem? { + val qualifiedName: String = psiClass.qualifiedName ?: psiClass.name!! + return classMap[qualifiedName] + } + + open fun findOrCreateClass(psiClass: PsiClass): PsiClassItem { + val existing = findClass(psiClass) + if (existing != null) { + return existing + } + + var curr = psiClass.containingClass + if (curr != null && findClass(curr) == null) { + // Make sure we construct outer/top level classes first + if (findClass(curr) == null) { + while (true) { + val containing = curr?.containingClass + if (containing == null) { + break + } else { + curr = containing + } + } + curr!! + createClass(curr) // this will also create inner classes, which should now be in the map + val inner = findClass(psiClass) + inner!! // should be there now + return inner + } + + } + + return existing ?: return createClass(psiClass) + } + + fun findClass(psiType: PsiType): PsiClassItem? { + if (psiType is PsiClassType) { + val cls = psiType.resolve() ?: return null + return findOrCreateClass(cls) + } + return null + } + + fun getClassType(cls: PsiClass): PsiClassType = getFactory().createType(cls) + + fun getComment(string: String, parent: PsiElement? = null): PsiDocComment = + getFactory().createDocCommentFromText(string, parent) + + fun getType(psiType: PsiType): PsiTypeItem { + // Note: We do *not* cache these; it turns out that storing PsiType instances + // in a map is bad for performance; it has a very expensive equals operation + // for some type comparisons (and we sometimes end up with unexpected results, + // e.g. where we fetch an "equals" type from the map but its representation + // is slightly different than we intended + return PsiTypeItem.create(this, psiType) + } + + fun getType(psiClass: PsiClass): PsiTypeItem { + return PsiTypeItem.create(this, getFactory().createType(psiClass)) + } + + private fun getPackageName(clz: PsiClass): String { + var top: PsiClass? = clz + while (top?.containingClass != null) { + top = top.containingClass + } + top ?: return "" + + val name = top.name + val fullName = top.qualifiedName ?: return "" + + return fullName.substring(0, fullName.length - 1 - name!!.length) + } + + fun findMethod(method: PsiMethod): PsiMethodItem { + val containingClass = method.containingClass + val cls = findOrCreateClass(containingClass!!) + + // Ensure initialized/registered via [#registerMethods] + if (methodMap[cls] == null) { + val map = HashMap<PsiMethod, PsiMethodItem>(40) + registerMethods(cls.methods(), map) + registerMethods(cls.constructors(), map) + methodMap[cls] = map + } + + val methods = methodMap[cls]!! + val methodItem = methods[method] + if (methodItem == null) { + // Probably switched psi classes (e.g. used source PsiClass in registry but + // found duplicate class in .jar library and we're now pointing to it; in that + // case, find the equivalent method by signature + val psiClass = cls.psiClass + val updatedMethod = psiClass.findMethodBySignature(method, true) + val result = methods[updatedMethod!!] + if (result == null) { + val extra = PsiMethodItem.create(this, cls, updatedMethod) + methods.put(method, extra) + methods.put(updatedMethod, extra) + if (!initializing) { + extra.finishInitialization() + } + + return extra + } + return result + } + + return methodItem + } + + fun findField(field: PsiField): Item? { + val containingClass = field.containingClass ?: return null + val cls = findOrCreateClass(containingClass) + return cls.findField(field.name) + } + + private fun registerMethods(methods: List<MethodItem>, map: MutableMap<PsiMethod, PsiMethodItem>) { + for (method in methods) { + val psiMethod = (method as PsiMethodItem).psiMethod + map[psiMethod] = method + } + } + + fun createReferenceFromText(s: String, parent: PsiElement? = null): PsiJavaCodeReferenceElement = + getFactory().createReferenceFromText(s, parent) + + fun createPsiMethod(s: String, parent: PsiElement? = null): PsiMethod = + getFactory().createMethodFromText(s, parent) + + fun createPsiType(s: String, parent: PsiElement? = null): PsiType = + getFactory().createTypeFromText(s, parent) + + private fun createPsiAnnotation(s: String, parent: PsiElement? = null): PsiAnnotation = + getFactory().createAnnotationFromText(s, parent) + + fun createDocTagFromText(s: String): PsiDocTag = getFactory().createDocTagFromText(s) + + private fun getFactory() = JavaPsiFacade.getElementFactory(project) + + override fun createAnnotation( + @Language("JAVA") source: String, context: Item?, + mapName: Boolean + ): PsiAnnotationItem { + val psiAnnotation = createPsiAnnotation(source, context?.psi()) + return PsiAnnotationItem.create(this, psiAnnotation) + } + + override fun supportsDocumentation(): Boolean = true + + override fun toString(): String = description + + override fun filter(filterEmit: Predicate<Item>, filterReference: Predicate<Item>): Codebase = + filter(this, filterEmit, filterReference) + + companion object { + // This is on the companion object rather than as an instance method to make sure + // we don't accidentally call self-methods on the old codebase when we meant the + // new: this forces us to be explicit about which codebase we mean + fun filter( + oldCodebase: PsiBasedCodebase, + filterEmit: Predicate<Item>, + filterReference: Predicate<Item> + ): Codebase { + val newCodebase = LockedPsiBasedCodebase("Filtered ${oldCodebase.description}") as PsiBasedCodebase + with(newCodebase) { + project = oldCodebase.project + hiddenPackages = HashMap(oldCodebase.hiddenPackages) + packageMap = HashMap(PACKAGE_ESTIMATE) + packageClasses = HashMap(PACKAGE_ESTIMATE) + packageClasses[""] = ArrayList() + methodMap = HashMap(METHOD_ESTIMATE) + initializing = true + original = oldCodebase + units = oldCodebase.units + } + + val oldToNew: BiMap<PsiClassItem, PsiClassItem> = HashBiMap.create(30000) + + val newPackages = mutableListOf<PsiPackageItem>() + + val oldPackages = oldCodebase.packageMap.values + for (pkg in oldPackages) { + if (pkg.hidden) { + continue + } + var currentPackage: PsiPackageItem? = null + + for (cls in pkg.topLevelClasses()) { + if (cls.isFromClassPath()) { + continue + } + val classFilter = FilteredClassView(cls as PsiClassItem, filterEmit, filterReference) + if (classFilter.emit()) { + val newPackage = currentPackage ?: run { + val newPackage = PsiPackageItem.create(newCodebase, pkg) + currentPackage = newPackage + newPackages.add(newPackage) + newCodebase.packageMap[newPackage.qualifiedName()] = newPackage + newPackage + } + + // Bottom-up copy + val newClass = classFilter.create(newCodebase) + + // Register it and all inner classes in the class map + for (c in newClass.allInnerClasses(includeSelf = true)) { + newCodebase.classMap[c.qualifiedName()] = c as PsiClassItem + } + + newPackage.addClass(newClass) // (inner classes are not registered in the package) + + oldToNew.put(cls, newClass) + newClass.containingPackage = newPackage + } + } + } + + // Initialize super classes and super methods + for (cls in newCodebase.classMap.values) { + val originalClass = cls.source!! // should be set here during construction + val prevSuperClass = originalClass.filteredSuperClassType(filterReference) + val curr = prevSuperClass?.asClass() + if (curr != null) { + val superClassName = curr.qualifiedName() + val publicSuperClass: PsiClassItem? = newCodebase.classMap[superClassName] + cls.setSuperClass( + if (publicSuperClass == null) { + if (curr.isFromClassPath() && options.allowReferencingUnknownClasses) { + curr + } else { + reporter.report( + Errors.HIDDEN_SUPERCLASS, originalClass.psiClass, + "$cls has a super class " + + "that is excluded via filters: $superClassName" + ) + null + } + } else { + newCodebase.classMap[superClassName] = publicSuperClass + publicSuperClass + }, + PsiTypeItem.create(newCodebase, prevSuperClass as PsiTypeItem) + ) + } else { + // typically java.lang.Object + cls.setSuperClass(null, null) + } + + val psiClass = cls.psiClass + + val filtered = originalClass.filteredInterfaceTypes(filterReference) + if (filtered.isEmpty()) { + cls.setInterfaces(emptyList()) + } else { + val interfaceTypeList = mutableListOf<PsiTypeItem>() + for (type in filtered) { + interfaceTypeList.add(PsiTypeItem.create(newCodebase, type as PsiTypeItem)) + } + cls.setInterfaces(interfaceTypeList) + } + + val oldCls = oldCodebase.findOrCreateClass(cls.psiClass) + val oldDefaultConstructor = oldCls.defaultConstructor + if (oldDefaultConstructor != null) { + val newConstructor = cls.findConstructor(oldDefaultConstructor) as PsiConstructorItem? + if (newConstructor != null) { + cls.defaultConstructor = newConstructor + } else { + // Constructor picked before that isn't available here: recreate it + val recreated = cls.createDefaultConstructor() + + recreated.mutableModifiers().setPackagePrivate(true) + cls.defaultConstructor = recreated + } + } + + val constructors = cls.constructors().asSequence() + val methods = cls.methods().asSequence() + val allMethods = methods.plus(constructors) + + // Super constructors + for (method in constructors) { + val original = method.source as PsiConstructorItem + + val originalSuperConstructor = original.superConstructor + val superConstructor = + if (originalSuperConstructor != null && filterReference.test(originalSuperConstructor)) { + originalSuperConstructor + } else { + original.findDelegate(filterReference, true) + } + + if (superConstructor != null) { + val superConstructorClass = + newCodebase.classMap[superConstructor.containingClass().qualifiedName()] + if (superConstructorClass == null) { + // This class is not in the filtered codebase + if (superConstructor.isFromClassPath() && options.allowReferencingUnknownClasses) { + method.superConstructor = superConstructor + method.setSuperMethods(listOf(superConstructor)) + } else { + reporter.report( + Errors.HIDDEN_SUPERCLASS, psiClass, "$method has a super method " + + "in a class that is excluded via filters: " + + "${superConstructor.containingClass().qualifiedName()} " + ) + } + } else { + // Find corresponding super method + val newSuperConstructor = superConstructorClass.findMethod(superConstructor) + if (newSuperConstructor == null) { + reporter.report( + Errors.HIDDEN_SUPERCLASS, psiClass, "$method has a super method " + + "in a class that is not matched via filters: " + + "${superConstructor.containingClass().qualifiedName()} " + ) + } else { + val constructorItem = newSuperConstructor as PsiConstructorItem + method.superConstructor = constructorItem + method.setSuperMethods(listOf(constructorItem)) + } + } + } else { + method.setSuperMethods(emptyList()) + } + } + + // Super methods + for (method in methods) { + val original = method.source!! // should be set here + val list = mutableListOf<MethodItem>() + val superMethods = ArrayDeque<MethodItem>() + superMethods.addAll(original.superMethods()) + while (!superMethods.isEmpty()) { + val superMethod = superMethods.removeFirst() + if (filterReference.test(superMethod)) { + // Find corresponding method in the new filtered codebase + val superMethodClass = newCodebase.classMap[superMethod.containingClass().qualifiedName()] + if (superMethodClass == null) { + // This class is not in the filtered codebase + if (superMethod.isFromClassPath() && options.allowReferencingUnknownClasses) { + list.add(superMethod) + } else { + reporter.report( + Errors.HIDDEN_SUPERCLASS, psiClass, "$method has a super method " + + "in a class that is excluded via filters: " + + "${superMethod.containingClass().qualifiedName()} " + ) + } + } else { + // Find corresponding super method + val newSuperMethod = superMethodClass.findMethod(superMethod) + if (newSuperMethod == null) { + reporter.report( + Errors.HIDDEN_SUPERCLASS, psiClass, "$method has a super method " + + "in a class that is not matched via filters: " + + "${superMethod.containingClass().qualifiedName()} " + ) + } else { + list.add(newSuperMethod) + } + } + } else { + // Process its parents instead + superMethods.addAll(superMethod.superMethods()) + } + } + method.setSuperMethods(list) + } + + // Methods and constructors: initialize throws lists + for (method in allMethods) { + val original = method.source!! // should be set here + + val throwsTypes: List<PsiClassItem> = if (original.throwsTypes().isNotEmpty()) { + val list = ArrayList<PsiClassItem>() + + original.filteredThrowsTypes(filterReference).forEach { + val newCls = newCodebase.classMap[it.qualifiedName()] + if (newCls == null) { + if (it.isFromClassPath() && options.allowReferencingUnknownClasses) { + list.add(it as PsiClassItem) + } else { + reporter.report( + Errors.HIDDEN_SUPERCLASS, psiClass, "$newCls has a throws class " + + "that is excluded via filters: ${it.qualifiedName()}" + ) + } + } else { + list.add(newCls) + } + } + list + } else { + emptyList() + } + method.setThrowsTypes(throwsTypes) + + method.source = null + } + + cls.source = null + } + + val pkg: PsiPackageItem? = newCodebase.findPackage("") ?: run { + val psiPackage = JavaPsiFacade.getInstance(newCodebase.project).findPackage("") + if (psiPackage != null) { + PsiPackageItem.create(newCodebase, psiPackage, null) + } else { + null + } + } + pkg?.let { + newCodebase.emptyPackage = it + newCodebase.packageMap[""] = it + } + + newCodebase.addParentPackages(newCodebase.packageMap.values) + + newCodebase.initializing = false + + return newCodebase + } + } + + fun registerClass(cls: PsiClassItem) { + assert(classMap[cls.qualifiedName()] == null || classMap[cls.qualifiedName()] == cls) + + classMap[cls.qualifiedName()] = cls + } +} + +class FilteredClassView( + val cls: PsiClassItem, + private val filterEmit: Predicate<Item>, + private val filterReference: Predicate<Item> +) { + val innerClasses: Sequence<FilteredClassView> + val constructors: Sequence<MethodItem> + val methods: Sequence<MethodItem> + val fields: Sequence<FieldItem> + + init { + constructors = cls.constructors().asSequence().filter { filterEmit.test(it) } + methods = cls.methods().asSequence().filter { filterEmit.test(it) } + //fields = cls.fields().asSequence().filter { filterEmit.test(it) } + + fields = cls.filteredFields(filterEmit).asSequence() + innerClasses = cls.innerClasses() + .asSequence() + .filterIsInstance(PsiClassItem::class.java) + .map { FilteredClassView(it, filterEmit, filterReference) } + } + + fun create(codebase: PsiBasedCodebase): PsiClassItem { + return PsiClassItem.create(codebase, this) + } + + /** Will this class emit anything? */ + fun emit(): Boolean { + val emit = emitClass() + if (emit) { + return true + } + + return innerClasses.any { it.emit() } + } + + /** Does the body of this class (everything other than the inner classes) emit anything? */ + private fun emitClass(): Boolean { + val classEmpty = (constructors.none() && methods.none() && fields.none()) + return if (filterEmit.test(cls)) { + true + } else if (!classEmpty) { + filterReference.test(cls) + } else { + false + } + } +} + +class LockedPsiBasedCodebase(description: String = "Unknown") : PsiBasedCodebase(description) { + // Not yet locked + //override fun findClass(psiClass: PsiClass): PsiClassItem { + // val qualifiedName: String = psiClass.qualifiedName ?: psiClass.name!! + // return classMap[qualifiedName] ?: error("Attempted to register ${psiClass.name} in locked codebase") + //} +} \ No newline at end of file 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 new file mode 100644 index 0000000..f06e9f9 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2017 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.compatibility +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.CompilationUnit +import com.android.tools.metalava.model.ConstructorItem +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.TypeItem +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, + val psiClass: PsiClass, + private val name: String, + private val fullName: String, + private val qualifiedName: String, + private val hasImplicitDefaultConstructor: Boolean, + private val classType: ClassType, + modifiers: PsiModifierItem, + documentation: String +) : + PsiItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + element = psiClass + ), ClassItem { + lateinit var containingPackage: PsiPackageItem + + override fun containingPackage(): PackageItem = containingClass?.containingPackage() ?: containingPackage + override fun simpleName(): String = name + override fun fullName(): String = fullName + override fun qualifiedName(): String = qualifiedName + override fun isInterface(): Boolean = classType == ClassType.INTERFACE + override fun isAnnotationType(): Boolean = classType == ClassType.ANNOTATION_TYPE + override fun isEnum(): Boolean = classType == ClassType.ENUM + override fun hasImplicitDefaultConstructor(): Boolean = hasImplicitDefaultConstructor + + private var superClass: ClassItem? = null + private var superClassType: TypeItem? = null + override fun superClass(): ClassItem? = superClass + override fun superClassType(): TypeItem? = superClassType + + override fun setSuperClass(superClass: ClassItem?, superClassType: TypeItem?) { + this.superClass = superClass + this.superClassType = superClassType + } + + override var defaultConstructor: ConstructorItem? = null + + private var containingClass: PsiClassItem? = null + override fun containingClass(): PsiClassItem? = containingClass + fun setContainingClass(containingClass: ClassItem?) { + this.containingClass = containingClass as PsiClassItem? + } + + // TODO: Come up with a better scheme for how to compute this + override var included: Boolean = true + + override var hasPrivateConstructor: Boolean = false + + override fun interfaceTypes(): List<TypeItem> = interfaceTypes + + override fun setInterfaceTypes(interfaceTypes: List<TypeItem>) { + @Suppress("UNCHECKED_CAST") + setInterfaces(interfaceTypes as List<PsiTypeItem>) + } + + fun setInterfaces(interfaceTypes: List<PsiTypeItem>) { + this.interfaceTypes = interfaceTypes + } + + private var allInterfaces: List<ClassItem>? = null + + override fun allInterfaces(): Sequence<ClassItem> { + if (allInterfaces == null) { + val classes = mutableSetOf<PsiClass>() + var curr: PsiClass? = psiClass + while (curr != null) { + if (curr.isInterface && !classes.contains(curr)) { + classes.add(curr) + } + addInterfaces(classes, curr.interfaces) + curr = curr.superClass + } + val result = mutableListOf<ClassItem>() + for (cls in classes) { + val item = codebase.findOrCreateClass(cls) + result.add(item) + } + + allInterfaces = result + } + + return allInterfaces!!.asSequence() + } + + private fun addInterfaces(result: MutableSet<PsiClass>, interfaces: Array<out PsiClass>) { + for (itf in interfaces) { + if (itf.isInterface && !result.contains(itf)) { + result.add(itf) + addInterfaces(result, itf.interfaces) + val superClass = itf.superClass + if (superClass != null) { + addInterfaces(result, arrayOf(superClass)) + } + } + } + } + + private lateinit var innerClasses: List<PsiClassItem> + private lateinit var interfaceTypes: List<TypeItem> + private lateinit var constructors: List<PsiConstructorItem> + private lateinit var methods: List<PsiMethodItem> + private lateinit var fields: List<FieldItem> + + /** + * If this item was created by filtering down a different codebase, this temporarily + * points to the original item during construction. This is used to let us initialize + * for example throws lists later, when all classes in the codebase have been + * initialized. + */ + internal var source: PsiClassItem? = null + + override fun innerClasses(): List<PsiClassItem> = innerClasses + override fun constructors(): List<PsiConstructorItem> = constructors + override fun methods(): List<PsiMethodItem> = methods + override fun fields(): List<FieldItem> = fields + + override fun toType(): TypeItem { + return PsiTypeItem.create(codebase, codebase.getClassType(psiClass)) + } + + override fun hasTypeVariables(): Boolean = psiClass.hasTypeParameters() + + override fun typeParameterList(): String? { + return PsiTypeItem.typeParameterList(psiClass.typeParameterList) + } + + override fun typeArgumentClasses(): List<ClassItem> { + return PsiTypeItem.typeParameterClasses( + codebase, + psiClass.typeParameterList + ) + } + + override fun typeParameterNames(): List<String> { + if (!psiClass.hasTypeParameters()) { + return emptyList() + } + + val typeParameters = psiClass.typeParameters + val list = mutableListOf<String>() + for (parameter in typeParameters) { + list.add(parameter.name ?: continue) + } + + return list + } + + override val isTypeParameter: Boolean + get() = psiClass is PsiTypeParameter + + override fun getCompilationUnit(): CompilationUnit? { + if (isInnerClass()) { + return null + } + + val containingFile = psiClass.containingFile ?: return null + if (containingFile is PsiCompiledFile) { + 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() + } + } + } + + fun findMethod(template: MethodItem): MethodItem? { + if (template.isConstructor()) { + return findConstructor(template as ConstructorItem) + } + + methods().asSequence() + .filter { it.matches(template) } + .forEach { return it } + return null + } + + fun findConstructor(template: ConstructorItem): ConstructorItem? { + constructors().asSequence() + .filter { it.matches(template) } + .forEach { return it } + return null + } + + override fun findMethod(methodName: String, parameters: String): MethodItem? { + if (methodName == simpleName()) { + // Constructor + constructors() + .filter { parametersMatch(it, parameters) } + .forEach { return it } + } else { + methods() + .filter { it.name() == methodName && parametersMatch(it, parameters) } + .forEach { return it } + } + + return null + } + + private fun parametersMatch(method: MethodItem, description: String): Boolean { + val parameterStrings = Splitter.on(",").trimResults().omitEmptyStrings().splitToList(description) + val parameters = method.parameters() + if (parameters.size != parameterStrings.size) { + return false + } + for (i in 0 until parameters.size) { + var parameterString = parameterStrings[i] + val index = parameterString.indexOf('<') + if (index != -1) { + parameterString = parameterString.substring(0, index) + } + val parameter = parameters[i].type().toErasedTypeString() + if (parameter != parameterString) { + return false + } + } + + return true + } + + override fun findField(fieldName: String): FieldItem? { + return fields().firstOrNull { it.name() == fieldName } + } + + override fun finishInitialization() { + super.finishInitialization() + + for (method in methods) { + method.finishInitialization() + } + for (method in constructors) { + method.finishInitialization() + } + for (field in fields) { + // There may be non-Psi fields here later (thanks to addField) but not during construction + (field as PsiFieldItem).finishInitialization() + } + for (inner in innerClasses) { + inner.finishInitialization() + } + + val extendsListTypes = psiClass.extendsListTypes + if (!extendsListTypes.isEmpty()) { + val type = PsiTypeItem.create(codebase, extendsListTypes[0]) + this.superClassType = type + this.superClass = type.asClass() + } else { + val superType = psiClass.superClassType + if (superType is PsiType) { + this.superClassType = PsiTypeItem.create(codebase, superType) + this.superClass = this.superClassType?.asClass() + } + } + + // Add interfaces. If this class is an interface, it can implement both + // classes from the extends clause and from the implements clause. + val interfaces = psiClass.implementsListTypes + setInterfaces(if (interfaces.isEmpty() && extendsListTypes.size <= 1) { + emptyList() + } else { + val result = ArrayList<PsiTypeItem>(interfaces.size + extendsListTypes.size - 1) + val create: (PsiClassType) -> PsiTypeItem = { + val type = PsiTypeItem.create(codebase, it) + type.asClass() // ensure that we initialize classes eagerly too such that they're registered etc + type + } + (1 until extendsListTypes.size).mapTo(result) { create(extendsListTypes[it]) } + interfaces.mapTo(result) { create(it) } + result + }) + } + + override fun mapTypeVariables(target: ClassItem, reverse: Boolean): Map<String, String> { + val targetPsi = target.psi() as PsiClass + val maps = mapTypeVariablesToSuperclass( + psiClass, targetPsi, considerSuperClasses = true, + considerInterfaces = targetPsi.isInterface + ) ?: return emptyMap() + + if (maps.isEmpty()) { + return emptyMap() + } + + if (maps.size == 1) { + return maps[0] + } + + val first = maps[0] + val flattened = mutableMapOf<String, String>() + for (key in first.keys) { + var variable: String? = key + for (map in maps) { + val value = map[variable] + variable = value + if (value == null) { + break + } else { + flattened.put(key, value) + } + } + } + return flattened + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return other is ClassItem && qualifiedName == other.qualifiedName() + } + + /** + * Creates a constructor in this class + */ + override fun createDefaultConstructor(): ConstructorItem { + return PsiConstructorItem.createDefaultConstructor(codebase, this, psiClass) + } + + override fun createMethod(template: MethodItem): MethodItem { + val method = template as PsiMethodItem + + val replacementMap = mapTypeVariables(template.containingClass(), reverse = true) + + val newMethod: PsiMethodItem + if (replacementMap.isEmpty()) { + newMethod = PsiMethodItem.create(codebase, this, method) + } else { + val stub = method.toStub(replacementMap) + val psiMethod = codebase.createPsiMethod(stub, psiClass) + newMethod = PsiMethodItem.create(codebase, this, psiMethod) + newMethod.inheritedInterfaceMethod = method.inheritedInterfaceMethod + newMethod.documentation = method.documentation + } + + if (template.throwsTypes().isEmpty()) { + newMethod.setThrowsTypes(emptyList()) + } else { + val throwsTypes = mutableListOf<ClassItem>() + for (type in template.throwsTypes()) { + if (type.codebase === codebase) { + throwsTypes.add(type) + } else { + throwsTypes.add(codebase.findOrCreateClass(((type as PsiClassItem).psiClass))) + } + } + newMethod.setThrowsTypes(throwsTypes) + } + + return newMethod + } + + override fun addMethod(method: MethodItem) { + (methods as MutableList<PsiMethodItem>).add(method as PsiMethodItem) + } + + override fun hashCode(): Int = qualifiedName.hashCode() + + override fun toString(): String = "class ${qualifiedName()}" + + companion object { + fun create(codebase: PsiBasedCodebase, psiClass: PsiClass): PsiClassItem { + val simpleName = psiClass.name!! + val fullName = computeFullClassName(psiClass) + val qualifiedName = psiClass.qualifiedName ?: simpleName + val hasImplicitDefaultConstructor = hasImplicitDefaultConstructor(psiClass) + val classType = ClassType.getClassType(psiClass) + + val commentText = PsiItem.javadoc(psiClass) + val modifiers = modifiers(codebase, psiClass, commentText) + val item = PsiClassItem( + codebase = codebase, + psiClass = psiClass, + name = simpleName, + fullName = fullName, + qualifiedName = qualifiedName, + classType = classType, + hasImplicitDefaultConstructor = hasImplicitDefaultConstructor, + documentation = commentText, + modifiers = modifiers + ) + codebase.registerClass(item) + item.modifiers.setOwner(item) + + // Construct the children + val psiMethods = psiClass.methods + val methods: MutableList<PsiMethodItem> = ArrayList(psiMethods.size) + + if (classType == ClassType.ENUM) { + addEnumMethods(codebase, item, psiClass, methods) + } + + val constructors: MutableList<PsiConstructorItem> = ArrayList(5) + for (psiMethod in psiMethods) { + if (psiMethod.isPrivate() || psiMethod.isPackagePrivate()) { + item.hasPrivateConstructor = true + } + if (psiMethod.isConstructor) { + val constructor = PsiConstructorItem.create(codebase, item, psiMethod) + constructors.add(constructor) + } else { + val method = PsiMethodItem.create(codebase, item, psiMethod) + methods.add(method) + } + } + + if (hasImplicitDefaultConstructor) { + assert(constructors.isEmpty()) + constructors.add(PsiConstructorItem.createDefaultConstructor(codebase, item, psiClass)) + } + + val fields: MutableList<FieldItem> = mutableListOf() + val psiFields = psiClass.fields + if (!psiFields.isEmpty()) { + psiFields.asSequence() + .mapTo(fields) { + PsiFieldItem.create(codebase, item, it) + } + } + + if (classType == ClassType.INTERFACE) { + // All members are implicitly public, fields are implicitly static, non-static methods are abstract + for (method in methods) { + method.mutableModifiers().setPublic(true) + } + for (method in fields) { + val m = method.mutableModifiers() + m.setPublic(true) + m.setStatic(true) + } + } + + item.constructors = constructors + item.methods = methods + item.fields = fields + + val psiInnerClasses = psiClass.innerClasses + item.innerClasses = if (psiInnerClasses.isEmpty()) { + emptyList() + } else { + val result = psiInnerClasses.asSequence() + .map { + val inner = codebase.findOrCreateClass(it) + inner.containingClass = item + inner + } + .toMutableList() + result + } + + return item + } + + fun create(codebase: PsiBasedCodebase, classFilter: FilteredClassView): PsiClassItem { + val original = classFilter.cls + + val newClass = PsiClassItem( + codebase = codebase, + psiClass = original.psiClass, + name = original.name, + fullName = original.fullName, + qualifiedName = original.qualifiedName, + classType = original.classType, + hasImplicitDefaultConstructor = original.hasImplicitDefaultConstructor, + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers) + ) + + newClass.modifiers.setOwner(newClass) + codebase.registerClass(newClass) + newClass.source = original + + newClass.constructors = classFilter.constructors.map { + PsiConstructorItem.create(codebase, newClass, it as PsiConstructorItem) + }.toMutableList() + + newClass.methods = classFilter.methods.map { + PsiMethodItem.create(codebase, newClass, it as PsiMethodItem) + }.toMutableList() + + + newClass.fields = classFilter.fields.asSequence() + // Preserve sorting order for enums + .sortedBy { it.sortingRank }.map { + PsiFieldItem.create(codebase, newClass, it as PsiFieldItem) + }.toMutableList() + + + newClass.innerClasses = classFilter.innerClasses.map { + val newInnerClass = codebase.findClass(it.cls.qualifiedName) ?: it.create(codebase) + newInnerClass.containingClass = newClass + codebase.registerClass(newInnerClass) + newInnerClass + }.toMutableList() + + newClass.hasPrivateConstructor = classFilter.cls.hasPrivateConstructor + + return newClass + } + + private fun addEnumMethods( + codebase: PsiBasedCodebase, + classItem: PsiClassItem, + psiClass: PsiClass, + result: MutableList<PsiMethodItem> + ) { + // Add these two methods as overrides into the API; this isn't necessary but is done in the old + // API generator + // method public static android.graphics.ColorSpace.Adaptation valueOf(java.lang.String); + // method public static final android.graphics.ColorSpace.Adaptation[] values(); + + if (compatibility.defaultAnnotationMethods) { + // TODO: Skip if we already have these methods here (but that shouldn't happen; nobody would + // type this by hand) + addEnumMethod( + codebase, classItem, + psiClass, result, + "public static ${psiClass.qualifiedName} valueOf(java.lang.String s) { return null; }" + ) + addEnumMethod( + codebase, classItem, + psiClass, result, + "public static final ${psiClass.qualifiedName}[] values() { return null; }" + ) + } + } + + private fun addEnumMethod( + codebase: PsiBasedCodebase, + classItem: PsiClassItem, + psiClass: PsiClass, + result: MutableList<PsiMethodItem>, source: String + ) { + val psiMethod = codebase.createPsiMethod(source, psiClass) + result.add(PsiMethodItem.create(codebase, classItem, psiMethod)) + } + + /** + * Computes the "full" class name; this is not the qualified class name (e.g. with package) + * but for an inner class it includes all the outer classes + */ + private fun computeFullClassName(cls: PsiClass): String { + if (cls.containingClass == null) { + val name = cls.name + return name!! + } else { + val list = mutableListOf<String>() + var curr: PsiClass? = cls + while (curr != null) { + val name = curr.name + curr = if (name != null) { + list.add(name) + curr.containingClass + } else { + break + + } + } + return list.asReversed().asSequence().joinToString(separator = ".") { it } + } + } + + private fun hasImplicitDefaultConstructor(psiClass: PsiClass): Boolean { + val constructors = psiClass.constructors + if (constructors.isEmpty() && !psiClass.isInterface && !psiClass.isAnnotationType && !psiClass.isEnum) { + if (PsiUtil.hasDefaultConstructor(psiClass)) { + return true + } + + // The above method isn't always right; for example, for the ContactsContract.Presence class + // in the framework, which looks like this: + // @Deprecated + // public static final class Presence extends StatusUpdates { + // } + // javac makes a default constructor: + // public final class android.provider.ContactsContract$Presence extends android.provider.ContactsContract$StatusUpdates { + // public android.provider.ContactsContract$Presence(); + // } + // but the above method returns false. So add some of our own heuristics: + if (psiClass.hasModifierProperty(PsiModifier.FINAL) && !psiClass.hasModifierProperty( + PsiModifier.ABSTRACT + ) && + psiClass.hasModifierProperty(PsiModifier.PUBLIC) + ) { + return true + } + } + + return false + } + + fun mapTypeVariablesToSuperclass( + psiClass: PsiClass, + targetClass: PsiClass, + considerSuperClasses: Boolean = true, + considerInterfaces: Boolean = psiClass.isInterface + ): MutableList<Map<String, String>>? { + // TODO: Prune search if type doesn't have type arguments! + if (considerSuperClasses) { + val list = mapTypeVariablesToSuperclass( + psiClass.superClassType, targetClass, + considerSuperClasses, considerInterfaces + ) + if (list != null) { + return list + } + } + + if (considerInterfaces) { + for (interfaceType in psiClass.interfaceTypes) { + val list = mapTypeVariablesToSuperclass( + interfaceType, targetClass, + considerSuperClasses, considerInterfaces + ) + if (list != null) { + return list + } + } + } + + return null + } + + fun mapTypeVariablesToSuperclass( + type: JvmReferenceType?, + targetClass: PsiClass, + considerSuperClasses: Boolean = true, + considerInterfaces: Boolean = true + ): MutableList<Map<String, String>>? { + // TODO: Prune search if type doesn't have type arguments! + val superType = type as? PsiClassReferenceType + val superClass = superType?.resolve() + if (superClass != null) { + if (superClass == targetClass) { + val map = mapTypeVariablesToSuperclass(superType) + if (map != null) { + return mutableListOf(map) + } else { + return null + } + } else { + val list = mapTypeVariablesToSuperclass( + superClass, targetClass, considerSuperClasses, + considerInterfaces + ) + if (list != null) { + val map = mapTypeVariablesToSuperclass(superType) + if (map != null) { + list.add(map) + } + return list + } + } + } + + return null + } + + fun mapTypeVariablesToSuperclass(superType: PsiClassReferenceType?): Map<String, String>? { + superType ?: return null + + val map = mutableMapOf<String, String>() + val superClass = superType.resolve() + if (superClass != null && superType.hasParameters()) { + val superTypeParameters = superClass.typeParameters + superType.parameters.forEachIndexed { index, parameter -> + if (parameter is PsiClassReferenceType) { + val parameterClass = parameter.resolve() + if (parameterClass != null) { + val parameterName = parameterClass.qualifiedName ?: parameterClass.name ?: parameter.name + if (index < superTypeParameters.size) { + val superTypeParameter = superTypeParameters[index] + val superTypeName = superTypeParameter.qualifiedName ?: superTypeParameter.name + if (superTypeName != null) { + map.put(superTypeName, parameterName) + } + } + } + } + } + } + + return map + } + } +} + +fun PsiModifierListOwner.isPrivate(): Boolean = modifierList?.hasExplicitModifier(PsiModifier.PRIVATE) == true +fun PsiModifierListOwner.isPackagePrivate(): Boolean { + val modifiers = modifierList ?: return false + return !(modifiers.hasModifierProperty(PsiModifier.PUBLIC) || + modifiers.hasModifierProperty(PsiModifier.PROTECTED)) +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiConstructorItem.kt new file mode 100644 index 0000000..f0db643 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiConstructorItem.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2017 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.ConstructorItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpressionStatement +import com.intellij.psi.PsiKeyword +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiWhiteSpace +import java.util.function.Predicate + +class PsiConstructorItem( + codebase: PsiBasedCodebase, + psiMethod: PsiMethod, + containingClass: PsiClassItem, + name: String, + modifiers: PsiModifierItem, + documentation: String, + parameters: List<PsiParameterItem>, + returnType: PsiTypeItem, + private val implicitConstructor: Boolean = false +) : + PsiMethodItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + psiMethod = psiMethod, + containingClass = containingClass, + name = name, + returnType = returnType, + parameters = parameters + ), ConstructorItem { + + init { + if (implicitConstructor) { + setThrowsTypes(emptyList()) + } + } + + override fun isImplicitConstructor(): Boolean = implicitConstructor + override fun isConstructor(): Boolean = true + override var superConstructor: ConstructorItem? = null + + private var _superMethods: List<MethodItem>? = null + override fun superMethods(): List<MethodItem> { + if (_superMethods == null) { + val result = mutableListOf<MethodItem>() + psiMethod.findSuperMethods().mapTo(result) { codebase.findMethod(it) } + + if (result.isEmpty() && isConstructor() && containingClass().superClass() != null) { + // Try a little harder; psi findSuperMethod doesn't seem to find super constructors in + // some cases, but maybe we can find it by resolving actual super() calls! + // TODO: Port to UAST + var curr: PsiElement? = psiMethod.body?.firstBodyElement + while (curr != null && curr is PsiWhiteSpace) { + curr = curr.nextSibling + } + if (curr is PsiExpressionStatement && curr.expression is PsiMethodCallExpression && + curr.expression.firstChild?.lastChild is PsiKeyword && + curr.expression.firstChild?.lastChild?.text == "super" + ) { + val resolved = (curr.expression as PsiMethodCallExpression).resolveMethod() + if (resolved is PsiMethod) { + val superConstructor = codebase.findMethod(resolved) + result.add(superConstructor) + } + } + } + _superMethods = result + } + + return _superMethods!! + } + + fun findDelegate(predicate: Predicate<Item>, allowInexactMatch: Boolean = true): PsiConstructorItem? { + if (isImplicitConstructor()) { + // Delegate to parent implicit constructors + (containingClass().superClass() as? PsiClassItem)?.constructors()?.forEach { + if (it.implicitConstructor) { + if (predicate.test(it)) { + return it + } else { + return it.findDelegate(predicate, allowInexactMatch) + } + } + } + } + + val superPsiMethod = PsiConstructorItem.findSuperOrThis(psiMethod) + if (superPsiMethod != null) { + val superMethod = codebase.findMethod(superPsiMethod) as PsiConstructorItem + if (!predicate.test(superMethod)) { + return superMethod.findDelegate(predicate, allowInexactMatch) + } + return superMethod + } + + // Try to pick an alternative - for example adding package private bridging + // methods if the super class is in the same package + val constructors = (containingClass().superClass() as? PsiClassItem)?.constructors() + constructors?.forEach { constructor -> + if (predicate.test(constructor)) { + return constructor + } + val superMethod = constructor.findDelegate(predicate, allowInexactMatch) + if (superMethod != null) { + return superMethod + } + } + + return null + } + + companion object { + fun create( + codebase: PsiBasedCodebase, containingClass: PsiClassItem, + psiMethod: PsiMethod + ): PsiConstructorItem { + assert(psiMethod.isConstructor) + val name = psiMethod.name + val commentText = javadoc(psiMethod) + val modifiers = modifiers(codebase, psiMethod, commentText) + val parameters = psiMethod.parameterList.parameters.mapIndexed { index, parameter -> + PsiParameterItem.create(codebase, parameter, index) + } + + val constructor = PsiConstructorItem( + codebase = codebase, + psiMethod = psiMethod, + containingClass = containingClass, + name = name, + documentation = commentText, + modifiers = modifiers, + parameters = parameters, + returnType = codebase.getType(containingClass.psiClass), + implicitConstructor = false + ) + constructor.modifiers.setOwner(constructor) + return constructor + } + + fun createDefaultConstructor( + codebase: PsiBasedCodebase, + containingClass: PsiClassItem, + psiClass: PsiClass + ): PsiConstructorItem { + val name = psiClass.name!! + + val factory = JavaPsiFacade.getInstance(psiClass.project).elementFactory + val psiMethod = factory.createConstructor(name, psiClass) + val flags = containingClass.modifiers.getAccessFlags() + val modifiers = PsiModifierItem(codebase, flags, null) + + val item = PsiConstructorItem( + codebase = codebase, + psiMethod = psiMethod, + containingClass = containingClass, + name = name, + documentation = "", + modifiers = modifiers, + parameters = emptyList(), + returnType = codebase.getType(psiClass), + implicitConstructor = true + ) + modifiers.setOwner(item) + return item + } + + fun create( + codebase: PsiBasedCodebase, + containingClass: PsiClassItem, + original: PsiConstructorItem + ): PsiConstructorItem { + val constructor = PsiConstructorItem( + codebase = codebase, + psiMethod = original.psiMethod, + containingClass = containingClass, + name = original.name(), + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers), + parameters = PsiParameterItem.create(codebase, original.parameters()), + returnType = codebase.getType(containingClass.psiClass), + implicitConstructor = original.implicitConstructor + ) + + constructor.modifiers.setOwner(constructor) + constructor.source = original + + return constructor + } + + internal fun findSuperOrThis(psiMethod: PsiMethod): PsiMethod? { + val superMethods = psiMethod.findSuperMethods() + if (superMethods.isNotEmpty()) { + return superMethods[0] + } + +// WARNING: I've deleted private constructors from class model; may not be right for here! + + // TODO: Port to UAST + var curr: PsiElement? = psiMethod.body?.firstBodyElement + while (curr != null && curr is PsiWhiteSpace) { + curr = curr.nextSibling + } + if (curr is PsiExpressionStatement && curr.expression is PsiMethodCallExpression) { + val call = curr.expression as PsiMethodCallExpression + if (call.firstChild?.lastChild is PsiKeyword) { + val keyword = call.firstChild?.lastChild + // TODO: Check Kotlin! + if (keyword?.text == "super" || keyword?.text == "this") { + val resolved = call.resolveMethod() + if (resolved is PsiMethod) { + return resolved + } + } + } + } + + // TODO: Try to find a super call *anywhere* in the method + + // See if we have an implicit constructor in the parent that we can call +// psiMethod.containingClass?.constructors?.forEach { +// // PsiUtil.hasDefaultConstructor(psiClass) if (it.impl) +// } + + return null + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiFieldItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiFieldItem.kt new file mode 100644 index 0000000..1a65079 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiFieldItem.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 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.FieldItem +import com.android.tools.metalava.model.TypeItem +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiField +import com.intellij.psi.impl.JavaConstantExpressionEvaluator + +class PsiFieldItem( + override val codebase: PsiBasedCodebase, + private val psiField: PsiField, + private val containingClass: PsiClassItem, + private val name: String, + modifiers: PsiModifierItem, + documentation: String, + private val fieldType: PsiTypeItem, + private val isEnumConstant: Boolean, + private val initialValue: Any? +) : + PsiItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + element = psiField + ), FieldItem { + + override fun type(): TypeItem = fieldType + override fun initialValue(requireConstant: Boolean): Any? { + if (initialValue != null) { + return initialValue + } + val constant = psiField.computeConstantValue() + if (constant != null) { + return constant + } + + return if (!requireConstant) { + val initializer = psiField.initializer ?: return null + JavaConstantExpressionEvaluator.computeConstantExpression(initializer, false) + } else { + null + } + } + + override fun isEnumConstant(): Boolean = isEnumConstant + override fun name(): String = name + override fun containingClass(): ClassItem = containingClass + + override fun duplicate(targetContainingClass: ClassItem): PsiFieldItem { + val duplicated = create(codebase, targetContainingClass as PsiClassItem, psiField) + + // Preserve flags that may have been inherited (propagated) fro surrounding packages + if (targetContainingClass.hidden) { + duplicated.hidden = true + } + if (targetContainingClass.removed) { + duplicated.removed = true + } + if (targetContainingClass.docOnly) { + duplicated.docOnly = true + } + + return duplicated + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return other is FieldItem && name == other.name() && containingClass == other.containingClass() + } + + override fun hashCode(): Int { + return name.hashCode() + } + + override fun toString(): String = "field ${containingClass.fullName()}.${name()}" + + companion object { + fun create(codebase: PsiBasedCodebase, containingClass: PsiClassItem, psiField: PsiField): PsiFieldItem { + val name = psiField.name + val commentText = javadoc(psiField) + val modifiers = modifiers(codebase, psiField, commentText) + + val fieldType = codebase.getType(psiField.type) + val isEnumConstant = psiField is PsiEnumConstant + val initialValue = null // compute lazily + + val field = PsiFieldItem( + codebase = codebase, + psiField = psiField, + containingClass = containingClass, + name = name, + documentation = commentText, + modifiers = modifiers, + fieldType = fieldType, + isEnumConstant = isEnumConstant, + initialValue = initialValue + ) + field.modifiers.setOwner(field) + return field + } + + fun create(codebase: PsiBasedCodebase, containingClass: PsiClassItem, original: PsiFieldItem): PsiFieldItem { + val field = PsiFieldItem( + codebase = codebase, + psiField = original.psiField, + containingClass = containingClass, + name = original.name, + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers), + fieldType = PsiTypeItem.create(codebase, original.fieldType), + isEnumConstant = original.isEnumConstant, + initialValue = original.initialValue + ) + field.modifiers.setOwner(field) + return field + } + } +} \ 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 new file mode 100644 index 0000000..548bc2f --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2017 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.DefaultItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MutableModifierList +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.ParameterItem +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.PsiReference +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.javadoc.PsiInlineDocTag +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.uast.UElement +import org.jetbrains.uast.sourcePsiElement + +abstract class PsiItem( + override val codebase: PsiBasedCodebase, + val element: PsiElement, + override val modifiers: PsiModifierItem, + override var documentation: String +) : DefaultItem() { + + override val deprecated: Boolean get() = modifiers.isDeprecated() + + @Suppress("LeakingThis") // Documentation can change, but we don't want to pick up subsequent @docOnly mutations + override var docOnly = documentation.contains("@doconly") + @Suppress("LeakingThis") + override var removed = documentation.contains("@removed") + @Suppress("LeakingThis") + override var hidden = (documentation.contains("@hide") || documentation.contains("@pending") + || modifiers.hasHideAnnotations()) && !modifiers.hasShowAnnotation() + + override fun psi(): PsiElement? = element + + // TODO: Consider only doing this in tests! + override fun isFromClassPath(): Boolean { + return if (element is UElement) { + element.psi is PsiCompiledElement + } else { + element is PsiCompiledElement + } + } + + /** Get a mutable version of modifiers for this item */ + override fun mutableModifiers(): MutableModifierList = modifiers + + override fun findTagDocumentation(tag: String): String? { + if (element is PsiCompiledElement) { + return null + } + if (documentation.isBlank()) { + return null + } + + // We can't just use element.docComment here because we may have modified + // the comment and then the comment snapshot in PSI isn't up to date with our + // latest changes + val docComment = codebase.getComment(documentation) + val docTag = docComment.findTagByName(tag) ?: return null + val text = docTag.text + + // Trim trailing next line (javadoc *) + var index = text.length - 1 + while (index > 0) { + val c = text[index] + if (!(c == '*' || c.isWhitespace())) { + break + } + index-- + } + index++ + return if (index < text.length) { + text.substring(0, index) + } else { + text + } + } + + override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) { + if (comment.isBlank()) { + return + } + + // TODO: Figure out if an annotation should go on the return value, or on the method. + // For example; threading: on the method, range: on the return value. + // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc) + + if (this is ParameterItem) { + // For parameters, the documentation goes into the surrounding method's documentation! + // Find the right parameter location! + val parameterName = name() + val target = containingMethod() + target.appendDocumentation(comment, parameterName) + return + } + + documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append) + } + + private fun packageName(): String? { + var curr: Item? = this + while (curr != null) { + if (curr is PackageItem) { + return curr.qualifiedName() + } + curr = curr.parent() + } + + return null + } + + override fun fullyQualifiedDocumentation(): String { + if (documentation.isBlank()) { + return documentation + } + + if (!(documentation.contains("@link") || // includes @linkplain + documentation.contains("@see") || + documentation.contains("@throws")) + ) { + // No relevant tags that need to be expanded/rewritten + return documentation + } + + val comment = + try { + codebase.getComment(documentation, psi()) + } catch (throwable: Throwable) { + // TODO: Get rid of line comments as documentation + // Invalid comment + if (documentation.startsWith("//") && documentation.contains("/**")) { + documentation = documentation.substring(documentation.indexOf("/**")) + } + codebase.getComment(documentation, psi()) + } + val sb = StringBuilder(documentation.length) + var curr = comment.firstChild + while (curr != null) { + if (curr is PsiDocTag) { + sb.append(getExpanded(curr)) + } else { + sb.append(curr.text) + } + curr = curr.nextSibling + } + + return sb.toString() + } + + private fun getExpanded(tag: PsiDocTag): String { + val text = tag.text + var valueElement = tag.valueElement + val reference = extractReference(tag) + var resolved = reference?.resolve() + var referenceText = reference?.element?.text + if (resolved == null && tag.name == "throws") { + // Workaround: @throws does not provide a valid reference to the class + val dataElements = tag.dataElements + if (dataElements.isNotEmpty()) { + if (dataElements[0] is PsiInlineDocTag) { + val innerReference = extractReference(dataElements[0] as PsiInlineDocTag) + resolved = innerReference?.resolve() + if (innerReference != null && resolved == null) { + referenceText = innerReference.canonicalText + resolved = codebase.createReferenceFromText(referenceText, psi()).resolve() + } else { + referenceText = innerReference?.element?.text + } + } + if (resolved == null || referenceText == null) { + val exceptionName = dataElements[0].text + val exceptionReference = codebase.createReferenceFromText(exceptionName, psi()) + resolved = exceptionReference.resolve() + referenceText = exceptionName + } else { + // Create a placeholder value since the inline tag + // wipes it out + val t = dataElements[0].text + val index = text.indexOf(t) + t.length + val suffix = text.substring(index) + val dummyTag = codebase.createDocTagFromText("@${tag.name} $suffix") + valueElement = dummyTag.valueElement + } + } else { + return text + } + } + + if (resolved != null && referenceText != null) { + if (referenceText.startsWith("#")) { + // Already a local/relative reference + return text + } + + when (resolved) { + // TODO: If same package, do nothing + // TODO: If not absolute, preserve syntax + is PsiClass -> { + if (samePackage(resolved)) { + return text + } + val qualifiedName = resolved.qualifiedName ?: return text + if (referenceText == qualifiedName) { + // Already absolute + return text + } + return when { + valueElement != null -> { + val start = valueElement.startOffsetInParent + val end = start + valueElement.textLength + text.substring(0, start) + qualifiedName + text.substring(end) + } + tag.name == "see" -> { + val suffix = text.substring(text.indexOf(referenceText) + referenceText.length) + "@see $qualifiedName$suffix" + } + text.startsWith("{") -> "{@${tag.name} $qualifiedName $referenceText}" + else -> "@${tag.name} $qualifiedName $referenceText" + } + } + is PsiMember -> { + val containing = resolved.containingClass ?: return text + if (samePackage(containing)) { + return text + } + val qualifiedName = containing.qualifiedName ?: return text + if (referenceText.startsWith(qualifiedName)) { + // Already absolute + return text + } + + val name = containing.name ?: return text + if (valueElement != null) { + val start = valueElement.startOffsetInParent + val close = text.lastIndexOf('}') + if (close == -1) { + return text // invalid javadoc + } + val memberPart = text.substring(text.indexOf(name, start) + name.length, close) + return "${text.substring(0, start)}$qualifiedName$memberPart $referenceText}" + } + } + } + } + + return text + } + + private fun samePackage(cls: PsiClass): Boolean { + val pkg = packageName() ?: return false + return cls.qualifiedName == "$pkg.${cls.name}" + } + + // Copied from UnnecessaryJavaDocLinkInspection + private fun extractReference(tag: PsiDocTag): PsiReference? { + val valueElement = tag.valueElement + if (valueElement != null) { + return valueElement.reference + } + // hack around the fact that a reference to a class is apparently + // not a PsiDocTagValue + val dataElements = tag.dataElements + if (dataElements.isEmpty()) { + return null + } + val salientElement: PsiElement = dataElements.firstOrNull { it !is PsiWhiteSpace } ?: return null + val child = salientElement.firstChild + return if (child !is PsiReference) null else child + } + + /** Finish initialization of the item */ + open fun finishInitialization() { + modifiers.setOwner(this) + } + + companion object { + fun javadoc(element: PsiElement): String { + if (element is PsiCompiledElement) { + return "" + } + + if (element is UElement) { + val comments = element.comments + if (comments.isNotEmpty()) { + val sb = StringBuilder() + comments.asSequence().joinTo(buffer = sb, separator = "\n") + return sb.toString() + } else { + // Temporary workaround: UAST seems to not return document nodes + // https://youtrack.jetbrains.com/issue/KT-22135 + val first = element.sourcePsiElement?.firstChild + if (first is KDoc) { + return first.text + } + + } + } + + if (element is PsiDocCommentOwner) { + return element.docComment?.text ?: "" + } + + return "" + } + + fun modifiers( + codebase: PsiBasedCodebase, + element: PsiModifierListOwner, + documentation: String + ): PsiModifierItem { + return PsiModifierItem.create(codebase, element, documentation) + } + + fun isKotlin(element: PsiElement): Boolean { + return element.language.id == "kotlin" + } + } +} 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 new file mode 100644 index 0000000..3c531b8 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2017 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.compatibility +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.TypeItem +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.TypeConversionUtil +import org.intellij.lang.annotations.Language +import java.io.StringWriter + +open class PsiMethodItem( + override val codebase: PsiBasedCodebase, + val psiMethod: PsiMethod, + private val containingClass: PsiClassItem, + private val name: String, + modifiers: PsiModifierItem, + documentation: String, + private val returnType: PsiTypeItem, + private val parameters: List<PsiParameterItem> +) : + PsiItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + element = psiMethod + ), MethodItem { + + init { + for (parameter in parameters) { + @Suppress("LeakingThis") + parameter.containingMethod = this + } + } + + /** + * If this item was created by filtering down a different codebase, this temporarily + * points to the original item during construction. This is used to let us initialize + * for example throws lists later, when all classes in the codebase have been + * initialized. + */ + internal var source: PsiMethodItem? = null + + override var inheritedInterfaceMethod: Boolean = false + + override fun name(): String = name + override fun containingClass(): PsiClassItem = containingClass + + override fun equals(other: Any?): Boolean { + // TODO: Allow mix and matching with other MethodItems? + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PsiMethodItem + + if (psiMethod != other.psiMethod) return false + + return true + } + + override fun hashCode(): Int { + return psiMethod.hashCode() + } + + override fun isConstructor(): Boolean = false + + override fun isImplicitConstructor(): Boolean = false + + override fun returnType(): TypeItem? = returnType + + override fun parameters(): List<ParameterItem> = parameters + + private var superMethods: List<MethodItem>? = null + override fun superMethods(): List<MethodItem> { + if (superMethods == null) { + val result = mutableListOf<MethodItem>() + psiMethod.findSuperMethods().mapTo(result) { codebase.findMethod(it) } + superMethods = result + } + + return superMethods!! + } + + fun setSuperMethods(superMethods: List<MethodItem>) { + this.superMethods = superMethods + } + + override fun typeParameterList(): String? { + return PsiTypeItem.typeParameterList(psiMethod.typeParameterList) + } + + override fun typeArgumentClasses(): List<ClassItem> { + return PsiTypeItem.typeParameterClasses(codebase, psiMethod.typeParameterList) + } + + // private var throwsTypes: List<ClassItem>? = null + private lateinit var throwsTypes: List<ClassItem> + + fun setThrowsTypes(throwsTypes: List<ClassItem>) { + this.throwsTypes = throwsTypes + } + + override fun throwsTypes(): List<ClassItem> = throwsTypes + + override fun duplicate(targetContainingClass: ClassItem): PsiMethodItem { + val duplicated = create(codebase, targetContainingClass as PsiClassItem, psiMethod) + + // Preserve flags that may have been inherited (propagated) fro surrounding packages + if (targetContainingClass.hidden) { + duplicated.hidden = true + } + if (targetContainingClass.removed) { + duplicated.removed = true + } + if (targetContainingClass.docOnly) { + duplicated.docOnly = true + } + + duplicated.throwsTypes = throwsTypes + return duplicated + } + + /* Call corresponding PSI utility method -- if I can find it! + override fun matches(other: MethodItem): Boolean { + if (other !is PsiMethodItem) { + return super.matches(other) + } + + // TODO: Find better API: this also checks surrounding class which we don't want! + return psiMethod.isEquivalentTo(other.psiMethod) + } + */ + + @Language("JAVA") + fun toStub(replacementMap: Map<String, String> = emptyMap()): String { + val method = this + // There are type variables; we have to recreate the method signature + val sb = StringBuilder(100) + + val modifierString = StringWriter() + ModifierList.write( + modifierString, method.modifiers, method, removeAbstract = false, + removeFinal = false, addPublic = true + ) + sb.append(modifierString.toString()) + + val typeParameters = typeParameterList() + if (typeParameters != null) { + sb.append(' ') + sb.append(TypeItem.convertTypeString(typeParameters, replacementMap)) + } + + val returnType = method.returnType() + sb.append(returnType?.convertTypeString(replacementMap)) + + sb.append(' ') + sb.append(method.name()) + + sb.append("(") + method.parameters().asSequence().forEachIndexed { i, parameter -> + if (i > 0) { + sb.append(", ") + } + + sb.append(parameter.type().convertTypeString(replacementMap)) + sb.append(' ') + sb.append(parameter.name()) + } + sb.append(")") + + val throws = method.throwsTypes().asSequence().sortedWith(ClassItem.fullNameComparator) + if (throws.any()) { + sb.append(" throws ") + throws.asSequence().sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type -> + if (i > 0) { + sb.append(", ") + } + // No need to replace variables; we can't have type arguments for exceptions + sb.append(type.qualifiedName()) + } + } + + sb.append(" { return ") + val defaultValue = PsiTypesUtil.getDefaultValueOfType(method.psiMethod.returnType) + sb.append(defaultValue) + sb.append("; }") + + return sb.toString() + } + + override fun finishInitialization() { + super.finishInitialization() + + throwsTypes = throwsTypes(codebase, psiMethod) + } + + companion object { + fun create( + codebase: PsiBasedCodebase, + containingClass: PsiClassItem, + psiMethod: PsiMethod + ): PsiMethodItem { + assert(!psiMethod.isConstructor) + val name = psiMethod.name + val commentText = javadoc(psiMethod) + val modifiers = modifiers(codebase, psiMethod, commentText) + val parameters = psiMethod.parameterList.parameters.mapIndexed { index, parameter -> + PsiParameterItem.create(codebase, parameter, index) + } + val returnType = codebase.getType(psiMethod.returnType!!) + val method = PsiMethodItem( + codebase = codebase, + psiMethod = psiMethod, + containingClass = containingClass, + name = name, + documentation = commentText, + modifiers = modifiers, + returnType = returnType, + parameters = parameters + ) + method.modifiers.setOwner(method) + return method + } + + fun create( + codebase: PsiBasedCodebase, + containingClass: PsiClassItem, + original: PsiMethodItem + ): PsiMethodItem { + val method = PsiMethodItem( + codebase = codebase, + psiMethod = original.psiMethod, + containingClass = containingClass, + name = original.name(), + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers), + returnType = PsiTypeItem.create(codebase, original.returnType), + parameters = PsiParameterItem.create(codebase, original.parameters()) + ) + method.modifiers.setOwner(method) + method.source = original + method.inheritedInterfaceMethod = original.inheritedInterfaceMethod + + return method + } + + private fun throwsTypes(codebase: PsiBasedCodebase, psiMethod: PsiMethod): List<ClassItem> { + val interfaces = psiMethod.throwsList.referencedTypes + if (interfaces.isEmpty()) { + return emptyList() + } + + val result = ArrayList<ClassItem>(interfaces.size) + for (cls in interfaces) { + if (compatibility.useErasureInThrows) { + val erased = TypeConversionUtil.erasure(cls) + result.add(codebase.findClass(erased) ?: continue) + continue + } + + result.add(codebase.findClass(cls) ?: continue) + } + + // We're sorting the names here even though outputs typically do their own sorting, + // since for example the MethodItem.sameSignature check wants to do an element-by-element + // comparison to see if the signature matches, and that should match overrides even if + // they specify their elements in different orders. + result.sortWith(ClassItem.fullNameComparator) + return result + } + } + + override fun toString(): String = "${if (isConstructor()) "constructor" else "method"} ${ + containingClass.qualifiedName()}.${name()}(${parameters().joinToString { it.type().toSimpleType() }})" +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt new file mode 100644 index 0000000..66a27f9 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2017 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.AnnotationItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.MutableModifierList +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiModifierList +import com.intellij.psi.PsiModifierListOwner + +class PsiModifierItem( + override val codebase: Codebase, + var flags: Int = 0, + private var annotations: MutableList<AnnotationItem>? = null +) : ModifierList, MutableModifierList { + private lateinit var owner: Item + + private operator fun set(mask: Int, set: Boolean) { + flags = if (set) { + flags or mask + } else { + flags and mask.inv() + } + } + + private fun isSet(mask: Int): Boolean { + return flags and mask != 0 + } + + override fun annotations(): List<AnnotationItem> { + return annotations ?: emptyList() + } + + override fun owner(): Item { + return owner + } + + fun setOwner(owner: Item) { + this.owner = owner + } + + override fun isPublic(): Boolean { + return isSet(PUBLIC) + } + + override fun isProtected(): Boolean { + return isSet(PROTECTED) + } + + override fun isPrivate(): Boolean { + return isSet(PRIVATE) + } + + override fun isStatic(): Boolean { + return isSet(STATIC) + } + + override fun isAbstract(): Boolean { + return isSet(ABSTRACT) + } + + override fun isFinal(): Boolean { + return isSet(FINAL) + } + + override fun isNative(): Boolean { + return isSet(NATIVE) + } + + override fun isSynchronized(): Boolean { + return isSet(SYNCHRONIZED) + } + + override fun isStrictFp(): Boolean { + return isSet(STRICT_FP) + } + + override fun isTransient(): Boolean { + return isSet(TRANSIENT) + } + + override fun isVolatile(): Boolean { + return isSet(VOLATILE) + } + + override fun isDefault(): Boolean { + return isSet(DEFAULT) + } + + fun isDeprecated(): Boolean { + return isSet(DEPRECATED) + } + + override fun setPublic(public: Boolean) { + set(PUBLIC, public) + } + + override fun setProtected(protected: Boolean) { + set(PROTECTED, protected) + } + + override fun setPrivate(private: Boolean) { + set(PRIVATE, private) + } + + override fun setStatic(static: Boolean) { + set(STATIC, static) + } + + override fun setAbstract(abstract: Boolean) { + set(ABSTRACT, abstract) + } + + override fun setFinal(final: Boolean) { + set(FINAL, final) + } + + override fun setNative(native: Boolean) { + set(NATIVE, native) + } + + override fun setSynchronized(synchronized: Boolean) { + set(SYNCHRONIZED, synchronized) + } + + override fun setStrictFp(strictfp: Boolean) { + set(STRICT_FP, strictfp) + } + + override fun setTransient(transient: Boolean) { + set(TRANSIENT, transient) + } + + override fun setVolatile(volatile: Boolean) { + set(VOLATILE, volatile) + } + + override fun setDefault(default: Boolean) { + set(DEFAULT, default) + } + + fun setDeprecated(deprecated: Boolean) { + set(DEPRECATED, deprecated) + } + + override fun addAnnotation(annotation: AnnotationItem) { + if (annotations == null) { + annotations = mutableListOf() + } + annotations?.add(annotation) + } + + override fun removeAnnotation(annotation: AnnotationItem) { + annotations?.remove(annotation) + } + + override fun clearAnnotations(annotation: AnnotationItem) { + annotations?.clear() + } + + override fun isEmpty(): Boolean { + return flags and DEPRECATED.inv() == 0 // deprecated isn't a real modifier + } + + override fun isPackagePrivate(): Boolean { + return flags and (PUBLIC or PROTECTED or PRIVATE) == 0 + } + + fun getAccessFlags(): Int { + return flags and (PUBLIC or PROTECTED or PRIVATE) + } + + // Rename? It's not a full equality, it's whether an override's modifier set is significant + override fun equivalentTo(other: ModifierList): Boolean { + if (other is PsiModifierItem) { + val flags2 = other.flags + val mask = EQUIVALENCE_MASK + + // Skipping the "default" flag + // TODO: Compatibility: skipnative modiifier and skipstrictfp modifier flags! + //if (!compatibility.skipNativeModifier && isNative() != other.isNative()) return false + //if (!compatibility.skipStrictFpModifier && isStrictFp() != other.isStrictFp()) return false + return flags and mask == flags2 and mask + } + return false + } + + companion object { + const val PUBLIC = 1 shl 0 + const val PROTECTED = 1 shl 1 + const val PRIVATE = 1 shl 2 + const val STATIC = 1 shl 3 + const val ABSTRACT = 1 shl 4 + const val FINAL = 1 shl 5 + const val NATIVE = 1 shl 6 + const val SYNCHRONIZED = 1 shl 7 + const val STRICT_FP = 1 shl 8 + const val TRANSIENT = 1 shl 9 + const val VOLATILE = 1 shl 10 + const val DEFAULT = 1 shl 11 + const val DEPRECATED = 1 shl 12 + + private const val EQUIVALENCE_MASK = PUBLIC or PROTECTED or PRIVATE or STATIC or ABSTRACT or + FINAL or TRANSIENT or VOLATILE or SYNCHRONIZED or DEPRECATED + + fun create(codebase: PsiBasedCodebase, element: PsiModifierListOwner, documentation: String?): PsiModifierItem { + val modifiers = create( + codebase, + element.modifierList + ) + + if (documentation?.contains("@deprecated") == true || + // Check for @Deprecated annotation + ((element as? PsiDocCommentOwner)?.isDeprecated == true) + ) { + modifiers.setDeprecated(true) + } + + return modifiers + } + + fun create(codebase: PsiBasedCodebase, modifierList: PsiModifierList?): PsiModifierItem { + modifierList ?: return PsiModifierItem(codebase) + + var flags = 0 + if (modifierList.hasModifierProperty(PsiModifier.PUBLIC)) { + flags = flags or PUBLIC + } + if (modifierList.hasModifierProperty(PsiModifier.PROTECTED)) { + flags = flags or PROTECTED + } + if (modifierList.hasModifierProperty(PsiModifier.PRIVATE)) { + flags = flags or PRIVATE + } + if (modifierList.hasModifierProperty(PsiModifier.STATIC)) { + flags = flags or STATIC + } + if (modifierList.hasModifierProperty(PsiModifier.ABSTRACT)) { + flags = flags or ABSTRACT + } + if (modifierList.hasModifierProperty(PsiModifier.FINAL)) { + flags = flags or FINAL + } + if (modifierList.hasModifierProperty(PsiModifier.NATIVE)) { + flags = flags or NATIVE + } + if (modifierList.hasModifierProperty(PsiModifier.SYNCHRONIZED)) { + flags = flags or SYNCHRONIZED + } + if (modifierList.hasModifierProperty(PsiModifier.STRICTFP)) { + flags = flags or STRICT_FP + } + if (modifierList.hasModifierProperty(PsiModifier.TRANSIENT)) { + flags = flags or TRANSIENT + } + if (modifierList.hasModifierProperty(PsiModifier.VOLATILE)) { + flags = flags or VOLATILE + } + if (modifierList.hasModifierProperty(PsiModifier.DEFAULT)) { + flags = flags or DEFAULT + } + + val psiAnnotations = modifierList.annotations + return if (psiAnnotations.isEmpty()) { + PsiModifierItem(codebase, flags) + } else { + val annotations: MutableList<AnnotationItem> = + psiAnnotations.map { PsiAnnotationItem.create(codebase, it) }.toMutableList() + PsiModifierItem(codebase, flags, annotations) + } + } + + fun create(codebase: PsiBasedCodebase, original: PsiModifierItem): PsiModifierItem { + val originalAnnotations = original.annotations ?: return PsiModifierItem(codebase, original.flags) + val copy: MutableList<AnnotationItem> = ArrayList(originalAnnotations.size) + originalAnnotations.mapTo(copy) { PsiAnnotationItem.create(codebase, it as PsiAnnotationItem) } + return PsiModifierItem(codebase, original.flags, copy) + } + } +} diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiPackageItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiPackageItem.kt new file mode 100644 index 0000000..a4fa5e4 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiPackageItem.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017 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.PackageItem +import com.android.tools.metalava.options +import com.intellij.psi.PsiPackage + +class PsiPackageItem( + override val codebase: PsiBasedCodebase, + private val psiPackage: PsiPackage, + private val qualifiedName: String, + modifiers: PsiModifierItem, + documentation: String +) : + PsiItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + element = psiPackage + ), PackageItem { + // Note - top level classes only + private val classes: MutableList<PsiClassItem> = mutableListOf() + + override fun topLevelClasses(): Sequence<ClassItem> = classes.asSequence().filter { it.isTopLevelClass() } + + lateinit var containingPackageField: PsiPackageItem + + override var hidden: Boolean = super.hidden || options.hidePackages.contains(qualifiedName) + + override fun containingPackage(): PackageItem? { + return if (qualifiedName.isEmpty()) null else { + if (!::containingPackageField.isInitialized) { + var parentPackage = qualifiedName + while (true) { + val index = parentPackage.lastIndexOf('.') + if (index == -1) { + containingPackageField = codebase.findPackage("")!! + return containingPackageField + } + parentPackage = parentPackage.substring(0, index) + val pkg = codebase.findPackage(parentPackage) + if (pkg != null) { + containingPackageField = pkg + return pkg + } + } + + @Suppress("UNREACHABLE_CODE") + null + } else { + containingPackageField + } + } + } + + fun addClass(cls: PsiClassItem) { + if (!cls.isTopLevelClass()) { + // TODO: Stash in a list somewhere to make allClasses() faster? + return + } + + /* + // Temp debugging: + val q = cls.qualifiedName() + for (c in classes) { + if (q == c.qualifiedName()) { + assert(false, { "Unexpectedly found class $q already listed in $this" }) + return + } + } + */ + + classes.add(cls) + cls.containingPackage = this + } + + fun addClasses(classList: List<PsiClassItem>) { + for (cls in classList) { + addClass(cls) + } + } + + override fun qualifiedName(): String = qualifiedName + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return other is PackageItem && qualifiedName == other.qualifiedName() + } + + override fun hashCode(): Int = qualifiedName.hashCode() + + override fun toString(): String = "Package $qualifiedName" + + override fun finishInitialization() { + super.finishInitialization() + val initialClasses = ArrayList(classes) + var original = initialClasses.size // classes added after this point will have indices >= original + for (cls in initialClasses) { + cls.finishInitialization() + } + + // Finish initialization of any additional classes that were registered during + // the above initialization (recursively) + while (original < classes.size) { + val added = ArrayList(classes.subList(original, classes.size)) + original = classes.size + for (cls in added) { + cls.finishInitialization() + } + } + } + + companion object { + fun create(codebase: PsiBasedCodebase, psiPackage: PsiPackage, extraDocs: String?): PsiPackageItem { + val commentText = javadoc(psiPackage) + if (extraDocs != null) "\n$extraDocs" else "" + val modifiers = modifiers(codebase, psiPackage, commentText) + if (modifiers.isPackagePrivate()) { + modifiers.setPublic(true) // packages are always public (if not hidden explicitly with private) + } + val qualifiedName = psiPackage.qualifiedName + + val pkg = PsiPackageItem( + codebase = codebase, + psiPackage = psiPackage, + qualifiedName = qualifiedName, + documentation = commentText, + modifiers = modifiers + ) + pkg.modifiers.setOwner(pkg) + return pkg + } + + fun create(codebase: PsiBasedCodebase, original: PsiPackageItem): PsiPackageItem { + val pkg = PsiPackageItem( + codebase = codebase, + psiPackage = original.psiPackage, + qualifiedName = original.qualifiedName, + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers) + ) + pkg.modifiers.setOwner(pkg) + return pkg + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt new file mode 100644 index 0000000..d44184a --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 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.MethodItem +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.TypeItem +import com.intellij.psi.PsiParameter +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex + +class PsiParameterItem( + override val codebase: PsiBasedCodebase, + private val psiParameter: PsiParameter, + private val name: String, + override val parameterIndex: Int, + modifiers: PsiModifierItem, + documentation: String, + private val type: PsiTypeItem +) : PsiItem( + codebase = codebase, + modifiers = modifiers, + documentation = documentation, + element = psiParameter +), ParameterItem { + lateinit var containingMethod: PsiMethodItem + + override fun name(): String = name + + override fun publicName(): String? { + if (isKotlin(psiParameter)) { + if (name == "\$receiver") { + return null + } + return name + } else { + // Java: Look for @ParameterName annotation + val annotation = modifiers.annotations().firstOrNull { it.isParameterName() } + if (annotation != null) { + return annotation.attributes().firstOrNull()?.value?.value()?.toString() + } + } + + return null + } + + override fun type(): TypeItem = type + override fun containingMethod(): MethodItem = containingMethod + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return other is ParameterItem && parameterIndex == other.parameterIndex && containingMethod == other.containingMethod() + } + + override fun hashCode(): Int { + return parameterIndex + } + + override fun toString(): String = "parameter ${name()}" + + companion object { + fun create( + codebase: PsiBasedCodebase, + psiParameter: PsiParameter, + parameterIndex: Int + ): PsiParameterItem { + val name = psiParameter.name ?: "arg${psiParameter.parameterIndex() + 1}" + val commentText = "" // no javadocs on individual parameters + val modifiers = modifiers(codebase, psiParameter, commentText) + val type = codebase.getType(psiParameter.type) + val parameter = PsiParameterItem( + codebase = codebase, + psiParameter = psiParameter, + name = name, + parameterIndex = parameterIndex, + documentation = commentText, + modifiers = modifiers, + type = type + ) + parameter.modifiers.setOwner(parameter) + return parameter + } + + fun create( + codebase: PsiBasedCodebase, + original: PsiParameterItem + ): PsiParameterItem { + val parameter = PsiParameterItem( + codebase = codebase, + psiParameter = original.psiParameter, + name = original.name, + parameterIndex = original.parameterIndex, + documentation = original.documentation, + modifiers = PsiModifierItem.create(codebase, original.modifiers), + type = PsiTypeItem.create(codebase, original.type) + ) + parameter.modifiers.setOwner(parameter) + return parameter + } + + fun create( + codebase: PsiBasedCodebase, + original: List<ParameterItem> + ): List<PsiParameterItem> { + return original.map { create(codebase, it as PsiParameterItem) } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt new file mode 100644 index 0000000..e9ad7ba --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2017 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.lint.detector.api.LintUtils +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.doclava1.ApiPredicate +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.model.text.TextTypeItem +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiCapturedWildcardType +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiDisjunctionType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEllipsisType +import com.intellij.psi.PsiIntersectionType +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.PsiJavaToken +import com.intellij.psi.PsiLambdaExpressionType +import com.intellij.psi.PsiPrimitiveType +import com.intellij.psi.PsiRecursiveElementVisitor +import com.intellij.psi.PsiReferenceList +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeElement +import com.intellij.psi.PsiTypeParameter +import com.intellij.psi.PsiTypeParameterList +import com.intellij.psi.PsiTypeVisitor +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.PsiWildcardType +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.TypeConversionUtil + +/** Represents a type backed by PSI */ +class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, private val psiType: PsiType) : TypeItem { + private var toString: String? = null + private var toAnnotatedString: String? = null + private var toInnerAnnotatedString: String? = null + private var toErasedString: String? = null + private var asClass: ClassItem? = null + + override fun toString(): String { + return toTypeString() + } + + override fun toTypeString( + outerAnnotations: Boolean, + innerAnnotations: Boolean, + erased: Boolean + ): String { + assert(innerAnnotations || !outerAnnotations) // Can't supply outer=true,inner=false + + return if (erased) { + if (innerAnnotations || outerAnnotations) { + // Not cached: Not common + toTypeString(codebase, psiType, outerAnnotations, innerAnnotations, erased) + } else { + if (toErasedString == null) { + toErasedString = toTypeString(codebase, psiType, outerAnnotations, innerAnnotations, erased) + } + toErasedString!! + } + } else { + when { + outerAnnotations -> { + if (toAnnotatedString == null) { + toAnnotatedString = TypeItem.formatType( + toTypeString( + codebase, + psiType, + outerAnnotations, + innerAnnotations, + erased + ) + ) + } + toAnnotatedString!! + } + innerAnnotations -> { + if (toInnerAnnotatedString == null) { + toInnerAnnotatedString = TypeItem.formatType( + toTypeString( + codebase, + psiType, + outerAnnotations, + innerAnnotations, + erased + ) + ) + } + toInnerAnnotatedString!! + } + else -> { + if (toString == null) { + toString = TypeItem.formatType(getCanonicalText(psiType, annotated = false)) + } + toString!! + } + } + } + } + + override fun toErasedTypeString(): String { + return toTypeString(outerAnnotations = false, innerAnnotations = false, erased = true) + } + + override fun isTypeParameter(): Boolean { + return asClass()?.psi() is PsiTypeParameter + } + + override fun internalName(): String { + if (primitive) { + val signature = getPrimitiveSignature(toString()) + if (signature != null) { + return signature + } + } + val sb = StringBuilder() + appendJvmSignature(sb, psiType) + return sb.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + return when (other) { + is TypeItem -> toTypeString().replace(" ", "") == other.toTypeString().replace(" ", "") + else -> false + } + } + + override fun asClass(): ClassItem? { + if (asClass == null) { + asClass = codebase.findClass(psiType) + } + return asClass + } + + override fun hashCode(): Int { + return psiType.hashCode() + } + + override val primitive: Boolean + get() = psiType is PsiPrimitiveType + + override fun defaultValue(): Any? { + return PsiTypesUtil.getDefaultValue(psiType) + } + + override fun defaultValueString(): String { + return PsiTypesUtil.getDefaultValueOfType(psiType) + } + + override fun typeArgumentClasses(): List<ClassItem> { + if (primitive) { + return emptyList() + } + + val classes = mutableListOf<ClassItem>() + psiType.accept(object : PsiTypeVisitor<PsiType>() { + override fun visitType(type: PsiType?): PsiType? { + return type + } + + override fun visitClassType(classType: PsiClassType): PsiType? { + codebase.findClass(classType)?.let { + if (!it.isTypeParameter && !classes.contains(it)) { + classes.add(it) + } + } + for (type in classType.parameters) { + type.accept(this) + } + return classType + } + + override fun visitWildcardType(wildcardType: PsiWildcardType): PsiType? { + if (wildcardType.isExtends) { + wildcardType.extendsBound.accept(this) + } + if (wildcardType.isSuper) { + wildcardType.superBound.accept(this) + } + if (wildcardType.isBounded) { + wildcardType.bound?.accept(this) + } + return wildcardType + } + + override fun visitPrimitiveType(primitiveType: PsiPrimitiveType): PsiType? { + return primitiveType + } + + override fun visitEllipsisType(ellipsisType: PsiEllipsisType): PsiType? { + ellipsisType.componentType.accept(this) + return ellipsisType + } + + override fun visitArrayType(arrayType: PsiArrayType): PsiType? { + arrayType.componentType.accept(this) + return arrayType + } + + override fun visitLambdaExpressionType(lambdaExpressionType: PsiLambdaExpressionType): PsiType? { + for (superType in lambdaExpressionType.superTypes) { + superType.accept(this) + } + return lambdaExpressionType + } + + override fun visitCapturedWildcardType(capturedWildcardType: PsiCapturedWildcardType): PsiType? { + capturedWildcardType.upperBound.accept(this) + return capturedWildcardType + } + + override fun visitDisjunctionType(disjunctionType: PsiDisjunctionType): PsiType? { + for (type in disjunctionType.disjunctions) { + type.accept(this) + } + return disjunctionType + } + + override fun visitIntersectionType(intersectionType: PsiIntersectionType): PsiType? { + for (type in intersectionType.conjuncts) { + type.accept(this) + } + return intersectionType + } + }) + + return classes + } + + override fun convertType(replacementMap: Map<String, String>?, owner: Item?): TypeItem { + val s = convertTypeString(replacementMap) + return create(codebase, codebase.createPsiType(s, owner?.psi())) + } + + override fun hasTypeArguments(): Boolean = psiType is PsiClassType && psiType.hasParameters() + + companion object { + private fun getPrimitiveSignature(typeName: String): String? = when (typeName) { + "boolean" -> "Z" + "byte" -> "B" + "char" -> "C" + "short" -> "S" + "int" -> "I" + "long" -> "J" + "float" -> "F" + "double" -> "D" + "void" -> "V" + else -> null + } + + private fun appendJvmSignature( + buffer: StringBuilder, + type: PsiType? + ): Boolean { + if (type == null) { + return false + } + + val psiType = TypeConversionUtil.erasure(type) + + when (psiType) { + is PsiArrayType -> { + buffer.append('[') + appendJvmSignature(buffer, psiType.componentType) + } + is PsiClassType -> { + val resolved = psiType.resolve() ?: return false + if (!appendJvmTypeName(buffer, resolved)) { + return false + } + } + is PsiPrimitiveType -> buffer.append(getPrimitiveSignature(psiType.canonicalText)) + else -> return false + } + return true + } + + private fun appendJvmTypeName( + signature: StringBuilder, + outerClass: PsiClass + ): Boolean { + val className = LintUtils.getInternalName(outerClass) ?: return false + signature.append('L').append(className).append(';') + return true + } + + fun toTypeString( + codebase: Codebase, + type: PsiType, + outerAnnotations: Boolean, + innerAnnotations: Boolean, + erased: Boolean + ): String { + + if (erased) { + // Recurse with raw type and erase=false + return toTypeString( + codebase, + TypeConversionUtil.erasure(type), + outerAnnotations, + innerAnnotations, + false + ) + } + + if (outerAnnotations || innerAnnotations) { + val typeString = mapAnnotations(codebase, getCanonicalText(type, true)) + if (!outerAnnotations && typeString.contains("@")) { + // Temporary hack: should use PSI type visitor instead + return TextTypeItem.eraseAnnotations(typeString, false, true) + } + return typeString + + } else { + return type.canonicalText + } + } + + /** + * Replace annotations in the given type string with the mapped qualified names + * to [AnnotationItem.mapName] + */ + private fun mapAnnotations(codebase: Codebase, string: String): String { + var s = string + var offset = s.length + while (true) { + val start = s.lastIndexOf('@', offset) + if (start == -1) { + return s + } + var index = start + 1 + val length = string.length + while (index < length) { + val c = string[index] + if (c != '.' && !Character.isJavaIdentifierPart(c)) { + break + } + index++ + } + val annotation = string.substring(start + 1, index) + + val mapped = AnnotationItem.mapName(codebase, annotation, ApiPredicate(codebase)) + if (mapped != null) { + if (mapped != annotation) { + s = string.substring(0, start + 1) + mapped + s.substring(index) + } + } else { + var balance = 0 + // Find annotation end + while (index < length) { + val c = string[index] + if (c == '(') { + balance++ + } else if (c == ')') { + balance-- + if (balance == 0) { + index++ + break + } + } else if (c != ' ' && balance == 0) { + break + } + index++ + } + s = string.substring(0, start) + s.substring(index) + } + offset = start - 1 + } + } + + private fun getCanonicalText(type: PsiType, annotated: Boolean): String { + val typeString = type.getCanonicalText(annotated) + if (!annotated) { + return typeString + } + + val index = typeString.indexOf(".@") + if (index != -1) { + // Work around type bugs in PSI: when you have a type like this: + // @android.support.annotation.NonNull java.lang.Float) + // PSI returns + // @android.support.annotation.NonNull java.lang.@android.support.annotation.NonNull Float) + // + // + // ...but sadly it's less predictable; e.g. it can be + // java.util.List<@android.support.annotation.Nullable java.lang.String> + // PSI returns + // java.util.List<java.lang.@android.support.annotation.Nullable String> + + // Here we try to reverse this: + val end = typeString.indexOf(' ', index) + if (end != -1) { + val annotation = typeString.substring(index + 1, end) + if (typeString.lastIndexOf(annotation, index) == -1) { + // Find out where to place it + var ci = index + while (ci > 0) { + val c = typeString[ci] + if (c != '.' && !Character.isJavaIdentifierPart(c)) { + ci++ + break + } + ci-- + } + return typeString.substring(0, ci) + + annotation + " " + + typeString.substring(ci, index + 1) + + typeString.substring(end + 1) + } else { + return typeString.substring(0, index + 1) + typeString.substring(end + 1) + } + } + } + + return typeString + } + + fun create(codebase: PsiBasedCodebase, psiType: PsiType): PsiTypeItem { + return PsiTypeItem(codebase, psiType) + } + + fun create(codebase: PsiBasedCodebase, original: PsiTypeItem): PsiTypeItem { + return PsiTypeItem(codebase, original.psiType) + } + + fun typeParameterList(typeList: PsiTypeParameterList?): String? { + if (typeList != null && typeList.typeParameters.isNotEmpty()) { + // TODO: Filter the type list classes? Try to construct a typelist of a private API! + // We can't just use typeList.text here, because that just + // uses the declaration from the source, which may not be + // fully qualified - e.g. we might get + // <T extends View> instead of <T extends android.view.View> + // Therefore, we'll need to compute it ourselves; I can't find + // a utility for this + val sb = StringBuilder() + typeList.accept(object : PsiRecursiveElementVisitor() { + override fun visitElement(element: PsiElement) { + if (element is PsiTypeParameterList) { + val typeParameters = element.typeParameters + if (typeParameters.isEmpty()) { + return + } + sb.append("<") + var first = true + for (parameter in typeParameters) { + if (!first) { + sb.append(", ") + } + first = false + visitElement(parameter) + } + sb.append(">") + return + } else if (element is PsiTypeParameter) { + sb.append(element.name) + // TODO: How do I get super -- e.g. "Comparable<? super T>" + val extendsList = element.extendsList + val refList = extendsList.referenceElements + if (refList.isNotEmpty()) { + sb.append(" extends ") + var first = true + for (refElement in refList) { + if (!first) { + sb.append(" & ") + } else { + first = false + } + + if (refElement is PsiJavaCodeReferenceElement) { + visitElement(refElement) + continue + } + val resolved = refElement.resolve() + if (resolved is PsiClass) { + sb.append(resolved.qualifiedName ?: resolved.name) + resolved.typeParameterList?.accept(this) + } else { + sb.append(refElement.referenceName) + } + } + } else { + val extendsListTypes = element.extendsListTypes + if (extendsListTypes.isNotEmpty()) { + sb.append(" extends ") + var first = true + for (type in extendsListTypes) { + if (!first) { + sb.append(" & ") + } else { + first = false + } + val resolved = type.resolve() + if (resolved == null) { + sb.append(type.className) + } else { + sb.append(resolved.qualifiedName ?: resolved.name) + resolved.typeParameterList?.accept(this) + } + } + } + } + return + } else if (element is PsiJavaCodeReferenceElement) { + val resolved = element.resolve() + if (resolved is PsiClass) { + if (resolved.qualifiedName == null) { + sb.append(resolved.name) + } else { + sb.append(resolved.qualifiedName) + } + val typeParameters = element.parameterList + if (typeParameters != null) { + val typeParameterElements = typeParameters.typeParameterElements + if (typeParameterElements.isEmpty()) { + return + } + + // When reading in this from bytecode, the order is sometimes wrong + // (for example, for + // public interface BaseStream<T, S extends BaseStream<T, S>> + // the extends type BaseStream<T, S> will return the typeParameterElements + // as [S,T] instead of [T,S]. However, the typeParameters.typeArguments + // list is correct, so order the elements by the typeArguments array instead + + // Special case: just one type argument: no sorting issue + if (typeParameterElements.size == 1) { + sb.append("<") + var first = true + for (parameter in typeParameterElements) { + if (!first) { + sb.append(", ") + } + first = false + visitElement(parameter) + } + sb.append(">") + return + } + + // More than one type argument + + val typeArguments = typeParameters.typeArguments + if (typeArguments.isNotEmpty()) { + sb.append("<") + var first = true + for (parameter in typeArguments) { + if (!first) { + sb.append(", ") + } + first = false + // Try to match up a type parameter element + var found = false + for (typeElement in typeParameterElements) { + if (parameter == typeElement.type) { + found = true + visitElement(typeElement) + break + } + } + if (!found) { + // No type element matched: use type instead + val classType = PsiTypesUtil.getPsiClass(parameter) + if (classType != null) { + visitElement(classType) + } else { + sb.append(parameter.canonicalText) + } + } + } + sb.append(">") + } + } + return + } + } else if (element is PsiTypeElement) { + val type = element.type + if (type is PsiWildcardType) { + sb.append("?") + if (type.isBounded) { + if (type.isExtends) { + sb.append(" extends ") + sb.append(type.extendsBound.canonicalText) + } + if (type.isSuper) { + sb.append(" super ") + sb.append(type.superBound.canonicalText) + } + } + return + } + sb.append(type.canonicalText) + return + } else if (element is PsiJavaToken && element.tokenType == JavaTokenType.COMMA) { + sb.append(",") + if (compatibility.spaceAfterCommaInTypes) { + if (element.nextSibling == null || element.nextSibling !is PsiWhiteSpace) { + sb.append(" ") + } + } + return + } + if (element.firstChild == null) { // leaf nodes only + if (element is PsiCompiledElement) { + if (element is PsiReferenceList) { + val referencedTypes = element.referencedTypes + var first = true + for (referenceType in referencedTypes) { + if (first) { + first = false + } else { + sb.append(", ") + } + sb.append(referenceType.canonicalText) + } + } + } else { + sb.append(element.text) + } + } + super.visitElement(element) + } + }) + + val typeString = sb.toString() + return TypeItem.cleanupGenerics(typeString) + } + + return null + } + + fun typeParameterClasses(codebase: PsiBasedCodebase, typeList: PsiTypeParameterList?): List<ClassItem> { + if (typeList != null && typeList.typeParameters.isNotEmpty()) { + val list = mutableListOf<ClassItem>() + typeList.accept(object : PsiRecursiveElementVisitor() { + override fun visitElement(element: PsiElement) { + if (element is PsiTypeParameterList) { + val typeParameters = element.typeParameters + for (parameter in typeParameters) { + visitElement(parameter) + } + return + } else if (element is PsiTypeParameter) { + val extendsList = element.extendsList + val refList = extendsList.referenceElements + if (refList.isNotEmpty()) { + for (refElement in refList) { + if (refElement is PsiJavaCodeReferenceElement) { + visitElement(refElement) + continue + } + val resolved = refElement.resolve() + if (resolved is PsiClass) { + addRealClass( + list, + codebase.findOrCreateClass(resolved) + ) + resolved.typeParameterList?.accept(this) + } + } + } else { + val extendsListTypes = element.extendsListTypes + if (extendsListTypes.isNotEmpty()) { + for (type in extendsListTypes) { + val resolved = type.resolve() + if (resolved != null) { + addRealClass( + list, codebase.findOrCreateClass(resolved) + ) + resolved.typeParameterList?.accept(this) + } + } + } + } + return + } else if (element is PsiJavaCodeReferenceElement) { + val resolved = element.resolve() + if (resolved is PsiClass) { + addRealClass( + list, + codebase.findOrCreateClass(resolved) + ) + element.parameterList?.accept(this) + return + } + } else if (element is PsiTypeElement) { + val type = element.type + if (type is PsiWildcardType) { + if (type.isBounded) { + addRealClass( + codebase, + list, type.bound + ) + } + if (type.isExtends) { + addRealClass( + codebase, + list, type.extendsBound + ) + } + if (type.isSuper) { + addRealClass( + codebase, + list, type.superBound + ) + } + return + } + return + } + super.visitElement(element) + } + }) + + return list + } else { + return emptyList() + } + } + + private fun addRealClass(codebase: PsiBasedCodebase, classes: MutableList<ClassItem>, type: PsiType?) { + codebase.findClass(type ?: return)?.let { + addRealClass(classes, it) + } + } + + private fun addRealClass(classes: MutableList<ClassItem>, cls: ClassItem) { + if (!cls.isTypeParameter && !classes.contains(cls)) { // typically small number of items, don't need Set + classes.add(cls) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt new file mode 100644 index 0000000..4a8b138 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.model.AnnotationAttribute +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultAnnotationAttribute + +class TextBackedAnnotationItem( + override val codebase: Codebase, + source: String, + mapName: Boolean = true +) : AnnotationItem { + private val qualifiedName: String? + private val full: String + private val attributes: List<AnnotationAttribute> + + init { + val index = source.indexOf("(") + val annotationClass = if (index == -1) + source.substring(1) // Strip @ + else + source.substring(1, index) + + qualifiedName = if (mapName) AnnotationItem.mapName(codebase, annotationClass) else annotationClass + full = when { + qualifiedName == null -> "" + index == -1 -> "@" + qualifiedName + else -> "@" + qualifiedName + source.substring(index) + } + + attributes = if (index == -1) { + emptyList() + } else { + DefaultAnnotationAttribute.createList( + source.substring(index + 1, source.lastIndexOf(')')) + ) + } + } + + override fun qualifiedName(): String? = qualifiedName + override fun attributes(): List<AnnotationAttribute> = attributes + override fun toSource(): String = full +} diff --git a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt new file mode 100644 index 0000000..e677b24 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.ApiInfo +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.ClassItem +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.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.TypeItem +import java.util.function.Predicate + +class TextClassItem( + override val codebase: ApiInfo, + position: SourcePositionInfo = SourcePositionInfo.UNKNOWN, + isPublic: Boolean = false, + isProtected: Boolean = false, + isPrivate: Boolean = false, + isStatic: Boolean = false, + private var isInterface: Boolean = false, + isAbstract: Boolean = false, + private var isEnum: Boolean = false, + private var isAnnotation: Boolean = false, + isFinal: Boolean = false, + val qualifiedName: String = "", + private val qualifiedTypeName: String = qualifiedName, + var name: String = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1), + val annotations: List<String>? = null +) : TextItem( + codebase = codebase, + position = position, + modifiers = TextModifiers( + codebase = codebase, + annotationStrings = annotations, + public = isPublic, protected = isProtected, private = isPrivate, + static = isStatic, abstract = isAbstract, final = isFinal + ) +), ClassItem { + + init { + (modifiers as TextModifiers).owner = this + } + + override val isTypeParameter: Boolean = false + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClassItem) return false + + return qualifiedName == other.qualifiedName() + } + + override fun hashCode(): Int { + return qualifiedName.hashCode() + } + + override fun interfaceTypes(): List<TypeItem> = interfaceTypes + override fun allInterfaces(): Sequence<ClassItem> { + return interfaceTypes.asSequence().map { it.asClass() }.filterNotNull() + } + + private var innerClasses: List<ClassItem> = mutableListOf() + + override var defaultConstructor: ConstructorItem? = null + + override var hasPrivateConstructor: Boolean = false + + override fun innerClasses(): List<ClassItem> = innerClasses + + override fun hasImplicitDefaultConstructor(): Boolean { + return false + } + + override fun isInterface(): Boolean = isInterface + override fun isAnnotationType(): Boolean = isAnnotation + override fun isEnum(): Boolean = isEnum + + var containingClass: TextClassItem? = null + override fun containingClass(): ClassItem? = containingClass + + private var containingPackage: PackageItem? = null + + fun setContainingPackage(containingPackage: TextPackageItem) { + this.containingPackage = containingPackage + } + + fun setIsAnnotationType(isAnnotation: Boolean) { + this.isAnnotation = isAnnotation + } + + fun setIsEnum(isEnum: Boolean) { + this.isEnum = isEnum + } + + override fun containingPackage(): PackageItem = containingPackage ?: error(this) + + override fun toType(): TypeItem = codebase.obtainTypeFromString( +// TODO: No, handle List<String>[] + if (typeParameterList() != null) + qualifiedName() + "<" + typeParameterList() + ">" + else + qualifiedName() + ) + + override fun hasTypeVariables(): Boolean { + return typeInfo?.hasTypeArguments() ?: false + } + + override fun typeParameterList(): String? { +// TODO: No, handle List<String>[] + val s = typeInfo.toString() + val index = s.indexOf('<') + if (index != -1) { + return s.substring(index) + } + return null + } + + override fun typeParameterNames(): List<String> = codebase.unsupported() + + private var superClass: ClassItem? = null + private var superClassType: TypeItem? = null + + override fun superClass(): ClassItem? = superClass + override fun superClassType(): TypeItem? = superClassType + + override fun setSuperClass(superClass: ClassItem?, superClassType: TypeItem?) { + this.superClass = superClass + this.superClassType = superClassType + } + + override fun setInterfaceTypes(interfaceTypes: List<TypeItem>) { + this.interfaceTypes = interfaceTypes.toMutableList() + } + + override fun findMethod(methodName: String, parameters: String): MethodItem? = codebase.unsupported() + + override fun findField(fieldName: String): FieldItem? = codebase.unsupported() + + private var typeInfo: TextTypeItem? = null + fun setTypeInfo(typeInfo: TextTypeItem) { + this.typeInfo = typeInfo + } + + fun asTypeInfo(): TextTypeItem { + if (typeInfo == null) { + typeInfo = codebase.obtainTypeFromString(qualifiedTypeName) + } + return typeInfo!! + } + + private var interfaceTypes = mutableListOf<TypeItem>() + private val constructors = mutableListOf<ConstructorItem>() + private val methods = mutableListOf<MethodItem>() + private val fields = mutableListOf<FieldItem>() + + override fun constructors(): List<ConstructorItem> = constructors + override fun methods(): List<MethodItem> = methods + override fun fields(): List<FieldItem> = fields + + fun addInterface(intf: TypeItem) { + interfaceTypes.add(intf) + } + + fun addInterface(intf: TextClassItem) { + interfaceTypes.add(intf.toType()) + } + + fun addConstructor(constructor: TextConstructorItem) { + constructors += constructor + } + + fun addMethod(method: TextMethodItem) { + methods += method + } + + fun addField(field: TextFieldItem) { + fields += field + } + + fun addEnumConstant(field: TextFieldItem) { + field.setEnumConstant(true) + fields += field + } + + fun addInnerClass(cls: TextClassItem) { + innerClasses += cls + } + + override fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? { + // No filtering in signature files: we assume signature APIs + // have already been filtered and all items should match. + // This lets us load signature files and rewrite them using updated + // output formats etc. + return superClassType + } + + private var fullName: String = name + override fun simpleName(): String = name.substring(name.lastIndexOf('.') + 1) + override fun fullName(): String = fullName + override fun qualifiedName(): String = qualifiedName + override fun toString(): String = qualifiedName() + + companion object { + fun createClassStub(codebase: ApiInfo, name: String): TextClassItem = + TextClassItem(codebase = codebase, qualifiedName = name, isPublic = true).also { + addStubPackage( + name, + codebase, + it + ) + } + + private fun addStubPackage( + name: String, codebase: Codebase, + textClassItem: TextClassItem + ) { + val pkgPath = name.substring(0, name.lastIndexOf('.')) + val pkg = codebase.findPackage(pkgPath) as? TextPackageItem ?: TextPackageItem( + codebase, + pkgPath, + SourcePositionInfo.UNKNOWN + ) + textClassItem.setContainingPackage(pkg) + } + + fun createInterfaceStub(codebase: ApiInfo, name: String): TextClassItem = + TextClassItem(isInterface = true, codebase = codebase, qualifiedName = name, isPublic = true).also { + addStubPackage( + name, + codebase, + it + ) + } + } +} diff --git a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt new file mode 100644 index 0000000..5dd2c02 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.ConstructorItem + +class TextConstructorItem( + codebase: Codebase, + name: String, + containingClass: TextClassItem, + isPublic: Boolean, + isProtected: Boolean, + isPrivate: Boolean, + isFinal: Boolean, + isStatic: Boolean, + isAbstract: Boolean, + isSynchronized: Boolean, + isNative: Boolean, + isDefault: Boolean, + returnType: TextTypeItem?, + position: SourcePositionInfo, + annotations: List<String>? +) : TextMethodItem( + codebase, name, containingClass, isPublic, isProtected, isPrivate, + isFinal, isStatic, isAbstract, isSynchronized, isNative, isDefault, returnType, position, annotations +), + ConstructorItem { + override var superConstructor: ConstructorItem? = null + + override fun isConstructor(): Boolean = true +} + + diff --git a/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt new file mode 100644 index 0000000..0180cd9 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.TypeItem + +class TextFieldItem( + codebase: Codebase, + name: String, + containingClass: TextClassItem, + isPublic: Boolean, + isProtected: Boolean, + isPrivate: Boolean, + isFinal: Boolean, isStatic: Boolean, + isTransient: Boolean, + isVolatile: Boolean, + private val type: TextTypeItem, + private val constantValue: Any?, + position: SourcePositionInfo, + annotations: List<String>? +) : TextMemberItem( + codebase, name, containingClass, position, + TextModifiers( + codebase = codebase, + annotationStrings = annotations, + public = isPublic, protected = isProtected, private = isPrivate, + static = isStatic, final = isFinal, transient = isTransient, volatile = isVolatile + ) +), FieldItem { + + init { + (modifiers as TextModifiers).owner = this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is FieldItem) return false + + if (name() != other.name()) { + return false + } + + return containingClass() == other.containingClass() + } + + override fun hashCode(): Int = name().hashCode() + + override fun type(): TypeItem = type + + override fun initialValue(requireConstant: Boolean): Any? = constantValue + + override fun toString(): String = "Field ${containingClass().fullName()}.${name()}" + + override fun duplicate(targetContainingClass: ClassItem): FieldItem = codebase.unsupported() + + private var isEnumConstant = false + override fun isEnumConstant(): Boolean = isEnumConstant + fun setEnumConstant(isEnumConstant: Boolean) { + this.isEnumConstant = isEnumConstant + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextItem.kt new file mode 100644 index 0000000..5caea45 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextItem.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultItem +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.MutableModifierList + +abstract class TextItem( + override val codebase: Codebase, + val position: SourcePositionInfo, + override var docOnly: Boolean = false, + override var documentation: String = "", + override var modifiers: ModifierList +) : DefaultItem() { + + override var hidden = false + override var removed = false + + override fun findTagDocumentation(tag: String): String? = null + override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) = codebase.unsupported() + override fun mutableModifiers(): MutableModifierList = modifiers as MutableModifierList + + private var mutableDeprecated = false + override val deprecated + get() = mutableDeprecated + + fun setDeprecated(deprecated: Boolean) { + mutableDeprecated = deprecated + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt new file mode 100644 index 0000000..71f97b4 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.MemberItem +import com.android.tools.metalava.model.ModifierList + +abstract class TextMemberItem( + codebase: Codebase, + private val name: String, + private val containingClass: TextClassItem, + position: SourcePositionInfo, + override var modifiers: ModifierList +) : TextItem(codebase, position = position, modifiers = modifiers), MemberItem { + + override fun name(): String = name + override fun containingClass(): ClassItem = containingClass +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt new file mode 100644 index 0000000..46c26a0 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.ParameterItem +import com.android.tools.metalava.model.TypeItem +import java.util.function.Predicate + +open class TextMethodItem( + codebase: Codebase, + name: String, + containingClass: TextClassItem, + isPublic: Boolean, + isProtected: Boolean, + isPrivate: Boolean, + isFinal: Boolean, + isStatic: Boolean, + isAbstract: Boolean, + isSynchronized: Boolean, + isNative: Boolean, + isDefault: Boolean, + private val returnType: TextTypeItem?, + position: SourcePositionInfo, + annotations: List<String>? +) : TextMemberItem( + // Explicitly coerce 'final' state of Java6-compiled enum values() method, to match + // the Java5-emitted base API description. + codebase, name, containingClass, position, + modifiers = TextModifiers( + codebase = codebase, + annotationStrings = annotations, public = isPublic, protected = isProtected, + private = isPrivate, static = isStatic, final = isFinal, abstract = isAbstract, + synchronized = isSynchronized, native = isNative, default = isDefault + ) +), MethodItem { + + init { + (modifiers as TextModifiers).owner = this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MethodItem) return false + + if (name() != other.name()) { + return false + } + + if (containingClass() != other.containingClass()) { + return false + } + + val parameters1 = parameters() + val parameters2 = other.parameters() + + if (parameters1.size != parameters2.size) { + return false + } + + for (i in 0 until parameters1.size) { + val parameter1 = parameters1[i] + val parameter2 = parameters2[i] + if (parameter1.type() != parameter2.type()) { + return false + } + } + return true + } + + override fun hashCode(): Int { + return name().hashCode() + } + + override fun isConstructor(): Boolean = false + + override fun returnType(): TypeItem? = returnType + + override fun superMethods(): List<MethodItem> = codebase.unsupported() + + override fun findPredicateSuperMethod(predicate: Predicate<Item>): MethodItem? = null + + private var typeParameterList: String? = null + + fun setTypeParameterList(typeParameterList: String?) { + this.typeParameterList = typeParameterList + } + + override fun typeParameterList(): String? = typeParameterList + + override fun duplicate(targetContainingClass: ClassItem): MethodItem = codebase.unsupported() + + private val throwsTypes = mutableListOf<String>() + private val parameters = mutableListOf<TextParameterItem>() + private var throwsClasses: List<ClassItem>? = null + + fun throwsTypeNames(): List<String> { + return throwsTypes + } + + override fun throwsTypes(): List<ClassItem> = if (throwsClasses == null) emptyList() else throwsClasses!! + + fun setThrowsList(throwsClasses: List<TextClassItem>) { + this.throwsClasses = throwsClasses + } + + override fun parameters(): List<ParameterItem> = parameters + + fun addException(throwsType: String) { + throwsTypes += throwsType + } + + fun addParameter(parameter: TextParameterItem) { + parameters += parameter + } + + private var varargs: Boolean = false + + fun setVarargs(varargs: Boolean) { + this.varargs = varargs + } + + fun isVarargs(): Boolean = varargs + + override var inheritedInterfaceMethod: Boolean = false + + override fun toString(): String = + "${if (isConstructor()) "Constructor" else "Method"} ${containingClass().qualifiedName()}.${name()}(${parameters().joinToString { + it.type().toSimpleType() + }})" +} + + diff --git a/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt new file mode 100644 index 0000000..5f3e3dd --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.model.AnnotationAttribute +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.DefaultAnnotationAttribute +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.ModifierList +import com.android.tools.metalava.model.MutableModifierList +import java.io.StringWriter + +class TextModifiers( + override val codebase: Codebase, + annotationStrings: List<String>? = null, + private var public: Boolean = false, + private var protected: Boolean = false, + private var private: Boolean = false, + private var static: Boolean = false, + private var abstract: Boolean = false, + private var final: Boolean = false, + private var native: Boolean = false, + private var synchronized: Boolean = false, + private var strictfp: Boolean = false, + private var transient: Boolean = false, + private var volatile: Boolean = false, + private var default: Boolean = false +) : MutableModifierList { + private var annotations: MutableList<AnnotationItem> = mutableListOf() + + init { + annotationStrings?.forEach { source -> + val index = source.indexOf('(') + val qualifiedName = AnnotationItem.mapName( + codebase, + if (index == -1) source.substring(1) else source.substring(1, index) + ) + + val attributes = + if (index == -1) { + emptyList() + } else { + DefaultAnnotationAttribute.createList(source.substring(index + 1, source.lastIndexOf(')'))) + } + val codebase = codebase + val item = object : AnnotationItem { + override val codebase = codebase + override fun attributes(): List<AnnotationAttribute> = attributes + override fun qualifiedName(): String? = qualifiedName + override fun toSource(): String = source + } + annotations.add(item) + } + } + + override fun isPublic(): Boolean = public + override fun isProtected(): Boolean = protected + override fun isPrivate(): Boolean = private + override fun isStatic(): Boolean = static + override fun isAbstract(): Boolean = abstract + override fun isFinal(): Boolean = final + override fun isNative(): Boolean = native + override fun isSynchronized(): Boolean = synchronized + override fun isStrictFp(): Boolean = strictfp + override fun isTransient(): Boolean = transient + override fun isVolatile(): Boolean = volatile + override fun isDefault(): Boolean = default + + override fun setPublic(public: Boolean) { + this.public = public + } + + override fun setProtected(protected: Boolean) { + this.protected = protected + } + + override fun setPrivate(private: Boolean) { + this.private = private + } + + override fun setStatic(static: Boolean) { + this.static = static + } + + override fun setAbstract(abstract: Boolean) { + this.abstract = abstract + } + + override fun setFinal(final: Boolean) { + this.final = final + } + + override fun setNative(native: Boolean) { + this.native = native + } + + override fun setSynchronized(synchronized: Boolean) { + this.synchronized = synchronized + } + + override fun setStrictFp(strictfp: Boolean) { + this.strictfp = strictfp + } + + override fun setTransient(transient: Boolean) { + this.transient = transient + } + + override fun setVolatile(volatile: Boolean) { + this.volatile = volatile + } + + override fun setDefault(default: Boolean) { + this.default = default + } + + var owner: Item? = null + + override fun owner(): Item = owner!! // Must be set after construction + override fun isEmpty(): Boolean { + return !(public || protected || private || static || abstract || final || native || synchronized + || strictfp || transient || volatile || default) + } + + override fun annotations(): List<AnnotationItem> { + return annotations + } + + override fun addAnnotation(annotation: AnnotationItem) { + val qualifiedName = annotation.qualifiedName() + if (annotations.any { it.qualifiedName() == qualifiedName }) { + return + } + // TODO: Worry about repeatable annotations? + annotations.add(annotation) + } + + override fun removeAnnotation(annotation: AnnotationItem) { + annotations.remove(annotation) + } + + override fun clearAnnotations(annotation: AnnotationItem) { + annotations.clear() + } + + override fun toString(): String { + val item = owner ?: return super.toString() + val writer = StringWriter() + ModifierList.write(writer, this, item) + return writer.toString() + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt new file mode 100644 index 0000000..c8559e0 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.PackageItem + +class TextPackageItem( + codebase: Codebase, + private val name: String, + position: SourcePositionInfo +) : TextItem(codebase, position, modifiers = TextModifiers(codebase = codebase, public = true)), PackageItem { + init { + (modifiers as TextModifiers).owner = this + } + + private val classes = ArrayList<TextClassItem>(100) + + fun name() = name + + fun addClass(classInfo: TextClassItem) { + classes.add(classInfo) + } + + internal fun classList(): List<ClassItem> = classes + + override fun topLevelClasses(): Sequence<ClassItem> = classes.asSequence() + + override fun qualifiedName(): String = name + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PackageItem) return false + + return name == other.qualifiedName() + } + + override fun hashCode(): Int { + return name.hashCode() + } + + override fun toString(): String = name +} diff --git a/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt new file mode 100644 index 0000000..fcf5531 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.doclava1.SourcePositionInfo +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.ParameterItem + +class TextParameterItem( + codebase: Codebase, + private val containingMethod: TextMethodItem, + private var name: String, + private var publicName: String?, + override val parameterIndex: Int, + var typeName: String, + private var type: TextTypeItem, + var vararg: Boolean, + position: SourcePositionInfo, + annotations: List<String>? +) +// TODO: We need to pass in parameter modifiers here (synchronized etc) + : TextItem( + codebase, position, + modifiers = TextModifiers(codebase = codebase, annotationStrings = annotations) +), ParameterItem { + + init { + (modifiers as TextModifiers).owner = this + } + + override var included: Boolean = true + override fun type(): TextTypeItem = type + override fun name(): String = name + override fun publicName(): String? = publicName + override fun containingMethod(): MethodItem = containingMethod + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ParameterItem) return false + + return parameterIndex == other.parameterIndex + } + + override fun hashCode(): Int = parameterIndex + + override fun toString(): String = "parameter ${name()}" +} diff --git a/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt new file mode 100644 index 0000000..94aa3bd --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2017 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.text + +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.TypeItem + +class TextTypeItem( + val codebase: Codebase, + val type: String +) : TypeItem { + override fun toString(): String = type + + override fun toErasedTypeString(): String { + return toTypeString(false, false, true) + } + + override fun toTypeString( + outerAnnotations: Boolean, + innerAnnotations: Boolean, + erased: Boolean + ): String { + return Companion.toTypeString(type, outerAnnotations, innerAnnotations, erased) + } + + override fun asClass(): ClassItem? { + val cls = toErasedTypeString() + return codebase.findClass(cls) + } + + fun qualifiedTypeName(): String = type + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TextTypeItem) return false + + return qualifiedTypeName() == other.qualifiedTypeName() + } + + override fun hashCode(): Int { + return qualifiedTypeName().hashCode() + } + + override val primitive: Boolean + get() = isPrimitive(type) + + override fun typeArgumentClasses(): List<ClassItem> = codebase.unsupported() + + override fun convertType(replacementMap: Map<String, String>?, owner: Item?): TypeItem { + return TextTypeItem(codebase, convertTypeString(replacementMap)) + } + + companion object { + fun toTypeString( + type: String, + outerAnnotations: Boolean, + innerAnnotations: Boolean, + erased: Boolean + ): String { + return if (erased) { + val raw = eraseTypeArguments(type) + if (outerAnnotations && innerAnnotations) { + raw + } else { + eraseAnnotations(raw, outerAnnotations, innerAnnotations) + } + } else { + if (outerAnnotations && innerAnnotations) { + type + } else { + eraseAnnotations(type, outerAnnotations, innerAnnotations) + } + } + } + + private fun eraseTypeArguments(s: String): String { + val index = s.indexOf('<') + if (index != -1) { + return s.substring(0, index) + } + return s + } + + fun eraseAnnotations(type: String, outer: Boolean, inner: Boolean): String { + if (type.indexOf('@') == -1) { + return type + } + + assert(inner || !outer) // Can't supply outer=true,inner=false + + // Assumption: top level annotations appear first + val length = type.length + var max = if (!inner) + length + else { + val space = type.indexOf(' ') + val generics = type.indexOf('<') + val first = if (space != -1) { + if (generics != -1) { + Math.min(space, generics) + } else { + space + } + } else { + generics + } + if (first != -1) { + first + } else { + length + } + } + + var s = type + while (true) { + val index = s.indexOf('@') + if (index == -1 || index >= max) { + return s + } + + // Find end + val end = findAnnotationEnd(s, index + 1) + val oldLength = s.length + s = s.substring(0, index).trim() + s.substring(end).trim() + val newLength = s.length + val removed = oldLength - newLength + max -= removed + } + } + + private fun findAnnotationEnd(type: String, start: Int): Int { + var index = start + val length = type.length + var balance = 0 + while (index < length) { + val c = type[index] + if (c == '(') { + balance++ + } else if (c == ')') { + balance-- + if (balance == 0) { + return index + 1 + } + } else if (c == '.') { + } else if (Character.isJavaIdentifierPart(c)) { + } else if (balance == 0) { + break + } + index++ + } + return index + } + + fun isPrimitive(type: String): Boolean { + return when (type) { + "byte", "char", "double", "float", "int", "long", "short", "boolean", "void", "null" -> true + else -> false + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/visitors/ApiVisitor.kt b/src/main/java/com/android/tools/metalava/model/visitors/ApiVisitor.kt new file mode 100644 index 0000000..3ee9389 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/ApiVisitor.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017 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.visitors + +import com.android.tools.metalava.doclava1.ApiPredicate +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.FieldItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MethodItem +import java.util.function.Predicate + +open class ApiVisitor( + /** + * Whether constructors should be visited as part of a [#visitMethod] call + * instead of just a [#visitConstructor] call. Helps simplify visitors that + * don't care to distinguish between the two cases. Defaults to true. + */ + visitConstructorsAsMethods: Boolean = true, + /** + * Whether inner classes should be visited "inside" a class; when this property + * is true, inner classes are visited before the [#afterVisitClass] method is + * called; when false, it's done afterwards. Defaults to false. + */ + nestInnerClasses: Boolean = false, + + /** + * Whether to include inherited fields too + */ + val inlineInheritedFields: Boolean = true, + + /** Comparator to sort methods with, or null to use natural (source) order */ + val methodComparator: Comparator<MethodItem>? = null, + + /** Comparator to sort fields with, or null to use natural (source) order */ + val fieldComparator: Comparator<FieldItem>? = null, + + /** The filter to use to determine if we should emit an item */ + val filterEmit: Predicate<Item>, + /** The filter to use to determine if we should emit a reference to an item */ + val filterReference: Predicate<Item> + +) : ItemVisitor(visitConstructorsAsMethods, nestInnerClasses) { + constructor( + codebase: Codebase, + /** + * Whether constructors should be visited as part of a [#visitMethod] call + * instead of just a [#visitConstructor] call. Helps simplify visitors that + * don't care to distinguish between the two cases. Defaults to true. + */ + visitConstructorsAsMethods: Boolean = true, + /** + * Whether inner classes should be visited "inside" a class; when this property + * is true, inner classes are visited before the [#afterVisitClass] method is + * called; when false, it's done afterwards. Defaults to false. + */ + nestInnerClasses: Boolean = false, + + /** Whether to ignore APIs with annotations in the --show-annotations list */ + ignoreShown: Boolean = true, + + /** Whether to match APIs marked for removal instead of the normal API */ + remove: Boolean = false, + + /** Comparator to sort methods with, or null to use natural (source) order */ + methodComparator: Comparator<MethodItem>? = null, + + /** Comparator to sort fields with, or null to use natural (source) order */ + fieldComparator: Comparator<FieldItem>? = null + ) : this( + visitConstructorsAsMethods, nestInnerClasses, + true, methodComparator, + fieldComparator, + ApiPredicate(codebase, ignoreShown = ignoreShown, matchRemoved = remove), + ApiPredicate(codebase, ignoreShown = true, ignoreRemoved = remove) + ) + + + // The API visitor lazily visits packages only when there's a match within at least one class; + // this property keeps track of whether we've already visited the current package + var visitingPackage = false + + open fun include(cls: ClassItem): Boolean = cls.emit +} + diff --git a/src/main/java/com/android/tools/metalava/model/visitors/ItemVisitor.kt b/src/main/java/com/android/tools/metalava/model/visitors/ItemVisitor.kt new file mode 100644 index 0000000..b04170b --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/ItemVisitor.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 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.visitors + +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.CompilationUnit +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.MethodItem +import com.android.tools.metalava.model.PackageItem +import com.android.tools.metalava.model.ParameterItem + +open class ItemVisitor( + /** + * Whether constructors should be visited as part of a [#visitMethod] call + * instead of just a [#visitConstructor] call. Helps simplify visitors that + * don't care to distinguish between the two cases. Defaults to true. + */ + val visitConstructorsAsMethods: Boolean = true, + /** + * Whether inner classes should be visited "inside" a class; when this property + * is true, inner classes are visited before the [#afterVisitClass] method is + * called; when false, it's done afterwards. Defaults to false. + */ + val nestInnerClasses: Boolean = false, + /** + * Whether to skip empty packages + */ + val skipEmptyPackages: Boolean = false +) { + + open fun skip(item: Item): Boolean = false + + /** Visits the item. This is always called before other more specialized visit methods, such as [visitClass]. */ + open fun visitItem(item: Item) {} + + open fun visitCompilationUnit(unit: CompilationUnit) {} + open fun visitPackage(pkg: PackageItem) {} + open fun visitClass(cls: ClassItem) {} + open fun visitConstructor(constructor: ConstructorItem) { + if (visitConstructorsAsMethods) { + visitMethod(constructor) + } + } + + open fun visitField(field: FieldItem) {} + open fun visitMethod(method: MethodItem) {} + open fun visitParameter(parameter: ParameterItem) {} + + open fun afterVisitItem(item: Item) {} + open fun afterVisitPackage(pkg: PackageItem) {} + open fun afterVisitCompilationUnit(unit: CompilationUnit) {} + open fun afterVisitClass(cls: ClassItem) {} + open fun afterVisitConstructor(constructor: ConstructorItem) { + if (visitConstructorsAsMethods) { + afterVisitMethod(constructor) + } + } + + open fun afterVisitField(field: FieldItem) {} + open fun afterVisitMethod(method: MethodItem) {} + open fun afterVisitParameter(parameter: ParameterItem) {} +} diff --git a/src/main/java/com/android/tools/metalava/model/visitors/PredicateVisitor.kt b/src/main/java/com/android/tools/metalava/model/visitors/PredicateVisitor.kt new file mode 100644 index 0000000..45ae3fa --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/PredicateVisitor.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 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.visitors + +import com.android.tools.metalava.model.Item + +open class PredicateVisitor( + private val predicate: (item: Item) -> Boolean, + + /** + * Whether constructors should be visited as part of a [#visitMethod] call + * instead of just a [#visitConstructor] call. Helps simplify visitors that + * don't care to distinguish between the two cases. Defaults to true. + */ + visitConstructorsAsMethods: Boolean = true, + /** + * Whether inner classes should be visited "inside" a class; when this property + * is true, inner classes are visited before the [#afterVisitClass] method is + * called; when false, it's done afterwards. Defaults to false. + */ + nestInnerClasses: Boolean = false +) : ItemVisitor(visitConstructorsAsMethods, nestInnerClasses) { + override fun skip(item: Item): Boolean { + return !predicate(item) + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/visitors/TypeVisitor.kt b/src/main/java/com/android/tools/metalava/model/visitors/TypeVisitor.kt new file mode 100644 index 0000000..510d2b6 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/TypeVisitor.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 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.visitors + +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.TypeItem + +open class TypeVisitor(val includeInterfaces: Boolean = false) { + open fun skip(item: Item): Boolean = false + open fun visitType(type: TypeItem, owner: Item) {} + open fun afterVisitType(type: TypeItem, owner: Item) {} +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/visitors/VisibleItemVisitor.kt b/src/main/java/com/android/tools/metalava/model/visitors/VisibleItemVisitor.kt new file mode 100644 index 0000000..923e34f --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/VisibleItemVisitor.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 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.visitors + +import com.android.tools.metalava.model.Item + +// TODO: Use ApiVisitor instead! +open class VisibleItemVisitor( + /** + * Whether constructors should be visited as part of a [#visitMethod] call + * instead of just a [#visitConstructor] call. Helps simplify visitors that + * don't care to distinguish between the two cases. Defaults to true. + */ + visitConstructorsAsMethods: Boolean = true, + /** + * Whether inner classes should be visited "inside" a class; when this property + * is true, inner classes are visited before the [#afterVisitClass] method is + * called; when false, it's done afterwards. Defaults to false. + */ + nestInnerClasses: Boolean = false +) : ItemVisitor(visitConstructorsAsMethods, nestInnerClasses) { + override fun skip(item: Item): Boolean { + return !item.included + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt b/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt new file mode 100644 index 0000000..c9ba501 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017 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.lint.checks.infrastructure.TestFiles.base64gzip +import com.android.tools.lint.checks.infrastructure.TestFiles.jar +import org.junit.Test + +class AnnotationStatisticsTest : DriverTest() { + @Test + + fun `Test emitting annotation statistics`() { + check( + extraArguments = arrayOf("--annotation-coverage-stats"), + expectedOutput = """ + Nullness Annotation Coverage Statistics: + 4 out of 6 methods were annotated (66%) + 0 out of 0 fields were annotated (0%) + 4 out of 5 parameters were annotated (80%) + """, + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public java.lang.Double convert0(java.lang.Float); + method @android.support.annotation.Nullable public java.lang.Double convert1(@android.support.annotation.NonNull java.lang.Float); + method @android.support.annotation.Nullable public java.lang.Double convert2(@android.support.annotation.NonNull java.lang.Float); + method @android.support.annotation.Nullable public java.lang.Double convert3(@android.support.annotation.NonNull java.lang.Float); + method @android.support.annotation.Nullable public java.lang.Double convert4(@android.support.annotation.NonNull java.lang.Float); + } + } + """ + ) + } + + @Test + fun `Test counting annotation usages of missing APIs`() { + check( + coverageJars = arrayOf( + /* + package test.pkg; + + public class ApiUsage { + ApiUsage() { + new ApiSurface(null).annotated1("Hello"); + new ApiSurface(null).missing1(null); + } + + Number myField = new ApiSurface(null, 5).missingField1; + + public void usage() { + ApiSurface apiSurface = new ApiSurface(null, 5); + apiSurface.annotated1("Hello"); + apiSurface.missing1(null); + apiSurface.missing2(null, 5); + apiSurface.missing3(5); + apiSurface.missing4(null); + apiSurface.missing5("Hello"); + } + } + */ + jar( + "libs/api-usage.jar", + base64gzip( + "test/pkg/ApiUsage.class", "" + + "H4sIAAAAAAAAAH1Ta28SQRQ9Q4Fd1qW0VPD9KNZKWe0WaH3VmBiTRpKNfqiW" + + "+HGgIw4uu4RdTPzqPzJRSDTxB/ijjHcGWqqlZjNn5945c86du7O/fn//CaCO" + + "xxZyuG7ghoUEbmYIVjNYREmFt0ysqfdtBesK7igoK9hQUDHgGLjLYPQ+7Unh" + + "HzLkvS7/yF2fBx335bDXEoNdhvQTGcj4KcNCeeOAIfk8PBQMOU8GYsJ5zVu+" + + "UJvDNvcP+ECqeJpMxu9lxLDixSKK3f6HjvusL99EvCNIOTVUEwaL9+X+cPCO" + + "tyko/EWdpols7YfDQVvsSSWbPVLZVAXbyGOFTOZsVEv3bGxi2caSgjxcMn4h" + + "fD+0sYWqjRrqNraxY+M+Hth4qHKPUGVYPlUzw9KsQa9aXdGOGYpl79/kbkN1" + + "KtuTUSSDjm4u6RXmEBXP4kEQxjwWirQ+j3Q6xWBO1c8SbswotbOKPMGpK5nG" + + "f522Z9MdrNI9y9ElZDSosYQJGvQdKHOeZl2k9NpWZQxW+YHEW2aOsfAVyW9I" + + "6XCMdN4YwWweRWyETPOLVioQFkkBSNKTIcUUSkjDhUF5wJ5o4wIu6houHft+" + + "Jr6qpHZs6TkTG0frO8wcwWo6hOd0ym7q9ewJ5xJM7WEhSydbJBf6y+iUaxRV" + + "6IxVcivqCrXTtAoLZVzGFaqD4arWuvYH9nECI6kDAAA=" + ) + ) + ), + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.support.annotation.NonNull; + import android.support.annotation.Nullable; + + public class ApiSurface { + ApiSurface(Object param) { + } + + ApiSurface(@Nullable Object param, int param2) { + } + + @Nullable public Object annotated1(@NonNull Object param1) { return null; } + public int annotated2(@NonNull Object param1, int param2) { return 1; } + + @NonNull public String annotatedField1 = ""; + public int annotatedField2; + public Number missingField1; + public Number missingField2; + + public int missing1(Object param1) { return 0; } + public int missing2(Object param1, int param2) { return 0; } + public Object missing3(int param1) { return null; } + @Nullable public Object missing4(Object param1) { return null; } + public Object missing5(@NonNull Object param1) { return null; } + + public class InnerClass { + @Nullable public Object annotated3(@NonNull Object param1) { return null; } + public int annotated4(@NonNull Object param1, int param2) { return 1; } + public int missing1(Object param1) { return 0; } + public int missing2(Object param1, int param2) { return 0; } + } + } + """ + ), + supportNonNullSource, + supportNullableSource + ), + expectedOutput = """ + 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 | + | ApiSurface.missingField1 | 1 | + | ApiSurface.missing2(Object, int) | 1 | + | ApiSurface.missing3(int) | 1 | + | ApiSurface.missing4(Object) | 1 | + | ApiSurface.missing5(Object) | 1 | + |--------------------------------------------------------------|------------------| + """, + compatibilityMode = false + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt new file mode 100644 index 0000000..129f41e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 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 org.junit.Test + +class AnnotationsMergerTest : DriverTest() { + + // TODO: Test what happens when we have conflicting data + // - NULLABLE_SOURCE on one non null on the other + // - annotation specified with different parameters (e.g @Size(4) vs @Size(6)) + + @Test + fun `Signature files contain annotations`() { + check( + compatibilityMode = false, + outputKotlinStyleNulls = false, + includeSystemApiAnnotations = false, + omitCommonPackages = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.support.annotation.NonNull; + import android.support.annotation.Nullable; + import android.annotation.IntRange; + import android.support.annotation.UiThread; + + @UiThread + public class MyTest { + public @Nullable Number myNumber; + public @Nullable Double convert(@NonNull Float f) { return null; } + public @IntRange(from=10,to=20) int clamp(int i) { return 10; } + }""" + ), + uiThreadSource, + intRangeAnnotationSource, + supportNonNullSource, + supportNullableSource + ), + // Skip the annotations themselves from the output + extraArguments = arrayOf( + "--hide-package", "android.annotation", + "--hide-package", "android.support.annotation" + ), + api = """ + package test.pkg { + @android.support.annotation.UiThread public class MyTest { + ctor public MyTest(); + method @android.support.annotation.IntRange(from=10, to=20) public int clamp(int); + method @android.support.annotation.Nullable public java.lang.Double convert(@android.support.annotation.NonNull java.lang.Float); + field @android.support.annotation.Nullable public java.lang.Number myNumber; + } + } + """ + ) + } + + @Test + fun `Merged class and method annotations with no arguments`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public class MyTest { + public Number myNumber; + public Double convert(Float f) { return null; } + public int clamp(int i) { return 10; } + } + """ + ) + ), + compatibilityMode = false, + outputKotlinStyleNulls = false, + omitCommonPackages = false, + mergeAnnotations = """<?xml version="1.0" encoding="UTF-8"?> + <root> + <item name="test.pkg.MyTest"> + <annotation name="android.support.annotation.UiThread" /> + </item> + <item name="test.pkg.MyTest java.lang.Double convert(java.lang.Float)"> + <annotation name="android.support.annotation.Nullable" /> + </item> + <item name="test.pkg.MyTest java.lang.Double convert(java.lang.Float) 0"> + <annotation name="android.support.annotation.NonNull" /> + </item> + <item name="test.pkg.MyTest myNumber"> + <annotation name="android.support.annotation.Nullable" /> + </item> + <item name="test.pkg.MyTest int clamp(int)"> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="10" /> + <val name="to" val="20" /> + </annotation> + </item> + </root> + """, + api = """ + package test.pkg { + @android.support.annotation.UiThread public class MyTest { + ctor public MyTest(); + method @android.support.annotation.IntRange(from=10, to=20) public int clamp(int); + method @android.support.annotation.Nullable public java.lang.Double convert(@android.support.annotation.NonNull java.lang.Float); + field @android.support.annotation.Nullable public java.lang.Number myNumber; + } + } + """ + ) + } +} diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt new file mode 100644 index 0000000..51a9027 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt @@ -0,0 +1,2091 @@ +/* + * Copyright (C) 2017 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.Ignore +import org.junit.Test + +class ApiFileTest : DriverTest() { +/* + Conditions to test: + - test all the error scenarios found in the notStrippable case! + - split up test into many individual test cases + - try referencing a class from an annotation! + - test having a throws list where some exceptions are hidden but extend + public exceptions: do we map over to the referenced ones? + + - test type reference from all the possible places -- in type signatures - interfaces, + extends, throws, type bounds, etc. + - method which overrides @hide method: should appear in subclass (test chain + of two nested too) + - BluetoothGattCharacteristic.java#describeContents: Was marked @hide, + but is unhidden because it extends a public interface method + - package javadoc (also make sure merging both!, e.g. try having @hide in each) + - StopWatchMap -- inner class with @hide marks allh top levels! + - Test field inlining: should I include fields from an interface, if that + inteface was implemented by the parent class (and therefore appears there too?) + What if the superclass is abstract? + - Exposing package private classes. Test that I only do this for package private + classes, NOT Those marked @hide (is that, having @hide on a used type, illegal?) + - Test error handling (invalid @hide combinations)) + - Consider what happens if we promote a package private class (because it's + extended by a public class), and then we restore its public members; the + override logic there isn't quite right. We've duplicated the significant-override + code to not skip private members, but that could change semantics. This isn't + ideal; instead we should now mark this class as public, and re-run the analysis + again (with the new hidden state for this class). + - compilation unit sorting - top level classes out of order + - Massive classes such as android.R.java? Maybe do synthetic test. + - HttpResponseCache implemented a public OkHttp interface, but the sole implementation + method was marked @hide, so the method doesn't show up. Is that some other rule -- + that we skip interfaces if their implementation methods are marked @hide? + - Test recursive package filtering. + */ + + @Test + fun `Basic class signature extraction`() { + // Basic class; also checks that default constructor is made explicit + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo { + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(); + } + } + """ + ) + } + + @Test + fun `Parameter Names in Java`() { + // Java code which explicitly specifies parameter names + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.ParameterName; + + public class Foo { + public void foo(int javaParameter1, @ParameterName("publicParameterName") int javaParameter2) { + } + } + """ + ), + supportParameterName + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(); + method public void foo(int, int publicParameterName); + } + } + """, + extraArguments = arrayOf("--hide-package", "android.support.annotation"), + checkDoclava1 = false /* doesn't support parameter names */ + ) + } + + @Test + fun `Basic Kotlin class`() { + check( + sourceFiles = *arrayOf( + kotlin( + """ + package test.pkg + class Kotlin(val property1: String = "Default Value", arg2: Int) : Parent() { + override fun method() = "Hello World" + fun otherMethod(ok: Boolean, times: Int) { + } + + var property2: String? = null + + private var someField = 42 + @JvmField + var someField2 = 42 + } + + open class Parent { + open fun method(): String? = null + open fun method2(value: Boolean, value: Boolean?): String? = null + open fun method3(value: Int?, value2: Int): Int = null + } + """ + ) + ), + api = """ + 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); + field public int someField2; + } + public class Parent { + ctor public Parent(); + method public java.lang.String method(); + method public java.lang.String method2(boolean value, java.lang.Boolean value); + method public int method3(java.lang.Integer value, int value2); + } + } + """, + checkDoclava1 = false /* doesn't support Kotlin... */ + ) + } + + @Ignore("Still broken: UAST is missing reified methods, and some missing symbol resolution") + @Test + fun `Kotlin Reified Methods`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public class Context { + @SuppressWarnings("unchecked") + public final <T> T getSystemService(Class<T> serviceClass) { + return null; + } + } + """ + ), + kotlin( + """ + package test.pkg + + inline fun <reified T> Context.systemService1() = getSystemService(T::class.java) + inline fun Context.systemService2() = getSystemService(String::class.java) + """ + ) + ), + api = """ + package test.pkg { + public class Context { + ctor public Context(); + method public final <T> T getSystemService(java.lang.Class<T>); + } + public final class _java_Kt { + ctor public _java_Kt(); + method public static final error.NonExistentClass systemService2(test.pkg.Context); + } + } + """, + checkDoclava1 = false /* doesn't support Kotlin... */ + ) + } + + @Test + fun `Propagate Platform types in Kotlin`() { + check( + compatibilityMode = false, + outputKotlinStyleNulls = true, + sourceFiles = *arrayOf( + kotlin( + """ + // Nullable Pair in Kotlin + package androidx.util + + class NullableKotlinPair<out F, out S>(val first: F?, val second: S?) + """ + ), + kotlin( + """ + // Non-nullable Pair in Kotlin + package androidx.util + class NonNullableKotlinPair<out F: Any, out S: Any>(val first: F, val second: S) + """ + ), + java( + """ + // Platform nullability Pair in Java + package androidx.util; + + @SuppressWarnings("WeakerAccess") + public class PlatformJavaPair<F, S> { + public final F first; + public final S second; + + public PlatformJavaPair(F first, S second) { + this.first = first; + this.second = second; + } + } + """ + ), + java( + """ + // Platform nullability Pair in Java + package androidx.util; + import android.support.annotation.NonNull; + import android.support.annotation.Nullable; + + @SuppressWarnings("WeakerAccess") + public class NullableJavaPair<F, S> { + public final @Nullable F first; + public final @Nullable S second; + + public NullableJavaPair(@Nullable F first, @Nullable S second) { + this.first = first; + this.second = second; + } + } + """ + ), + java( + """ + // Platform nullability Pair in Java + package androidx.util; + + import android.support.annotation.NonNull; + + @SuppressWarnings("WeakerAccess") + public class NonNullableJavaPair<F, S> { + public final @NonNull F first; + public final @NonNull S second; + + public NonNullableJavaPair(@NonNull F first, @NonNull S second) { + this.first = first; + this.second = second; + } + } + """ + ), + kotlin( + """ + package androidx.util + + @Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability. + inline operator fun <F, S> PlatformJavaPair<F, S>.component1() = first + """ + ), + supportNonNullSource, + supportNullableSource + ), + api = """ + package androidx.util { + public class NonNullableJavaPair<F, S> { + ctor public NonNullableJavaPair(F, S); + field public final F first; + field public final S second; + } + public final class NonNullableKotlinPair<F, S> { + ctor public NonNullableKotlinPair(F first, S second); + method public final F getFirst(); + method public final S getSecond(); + } + public class NullableJavaPair<F, S> { + ctor public NullableJavaPair(F?, S?); + field public final F? first; + field public final S? second; + } + public final class NullableKotlinPair<F, S> { + ctor public NullableKotlinPair(F? first, S? second); + method public final F? getFirst(); + method public final S? getSecond(); + } + public class PlatformJavaPair<F, S> { + ctor public PlatformJavaPair(F!, S!); + field public final F! first; + field public final S! second; + } + public final class TestKt { + ctor public TestKt(); + method public static final <F, S> F! component1(androidx.util.PlatformJavaPair<F,S>); + } + } + """, + extraArguments = arrayOf("--hide-package", "android.support.annotation"), + checkDoclava1 = false /* doesn't support Kotlin... */ + ) + } + + @Test + fun `Extract class with generics`() { + // Basic interface with generics; makes sure <T extends Object> is written as just <T> + // Also include some more complex generics expressions to make sure they're serialized + // correctly (in particular, using fully qualified names instead of what appears in + // the source code.) + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface<T extends Object> + extends MyBaseInterface { + } + """ + ), java( + """ + package a.b.c; + @SuppressWarnings("ALL") + public interface MyStream<T, S extends MyStream<T, S>> extends test.pkg.AutoCloseable { + } + """ + ), java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface2<T extends Number> + extends MyBaseInterface { + class TtsSpan<C extends MyInterface<?>> { } + abstract class Range<T extends Comparable<? super T>> { + protected String myString; + } + } + """ + ), + java( + """ + package test.pkg; + public interface MyBaseInterface { + void fun(int a, String b); + } + """ + ), + java( + """ + package test.pkg; + public interface MyOtherInterface extends MyBaseInterface, AutoCloseable { + void fun(int a, String b); + } + """ + ), + java( + """ + package test.pkg; + public interface AutoCloseable { + } + """ + ) + ), + api = """ + package a.b.c { + public abstract interface MyStream<T, S extends a.b.c.MyStream<T, S>> implements test.pkg.AutoCloseable { + } + } + package test.pkg { + public abstract interface AutoCloseable { + } + public abstract interface MyBaseInterface { + method public abstract void fun(int, java.lang.String); + } + public abstract interface MyInterface<T> implements test.pkg.MyBaseInterface { + } + public abstract interface MyInterface2<T extends java.lang.Number> implements test.pkg.MyBaseInterface { + } + public static abstract class MyInterface2.Range<T extends java.lang.Comparable<? super T>> { + ctor public MyInterface2.Range(); + field protected java.lang.String myString; + } + public static class MyInterface2.TtsSpan<C extends test.pkg.MyInterface<?>> { + ctor public MyInterface2.TtsSpan(); + } + public abstract interface MyOtherInterface implements test.pkg.AutoCloseable test.pkg.MyBaseInterface { + } + } + """ + ) + } + + @Test + fun `Basic class without default constructor, has constructors with args`() { + // Class without private constructors (shouldn't insert default constructor) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo { + public Foo(int i) { + + } + public Foo(int i, int j) { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(int); + ctor public Foo(int, int); + } + } + """ + ) + } + + @Test + fun `Basic class without default constructor, has private constructor`() { + // Class without private constructors; no default constructor should be inserted + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class Foo { + private Foo() { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + } + } + """ + ) + } + + @Test + fun `Interface class extraction`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface Foo { + void foo(); + } + """ + ) + ), + api = """ + package test.pkg { + public abstract interface Foo { + method public abstract void foo(); + } + } + """ + ) + } + + @Test + fun `Enum class extraction`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public enum Foo { + A, B; + } + """ + ) + ), + api = """ + package test.pkg { + public final class Foo extends java.lang.Enum { + method public static test.pkg.Foo valueOf(java.lang.String); + method public static final test.pkg.Foo[] values(); + enum_constant public static final test.pkg.Foo A; + enum_constant public static final test.pkg.Foo B; + } + } + """ + ) + } + + @Test + fun `Enum class, non-compat mode`() { + @Suppress("ConstantConditionIf") + if (SKIP_NON_COMPAT) { + println("Skipping test for non-compatibility mode which isn't fully done yet") + return + } + + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public enum Foo { + A, B; + } + """ + ) + ), + compatibilityMode = false, + api = """ + package test.pkg { + public enum Foo { + enum_constant public static final test.pkg.Foo! A; + enum_constant public static final test.pkg.Foo! B; + } + } + """ + ) + } + + @Test + fun `Annotation class extraction`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + check( + // For unknown reasons, doclava1 behaves differently here than when invoked on the + // whole platform + checkDoclava1 = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public @interface Foo { + String value(); + } + """ + ), + java( + """ + package android.annotation; + import static java.lang.annotation.ElementType.*; + import java.lang.annotation.*; + @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) + @Retention(RetentionPolicy.CLASS) + @SuppressWarnings("ALL") + public @interface SuppressLint { + String[] value(); + } + """ + ) + ), + api = """ + package android.annotation { + public abstract class SuppressLint implements java.lang.annotation.Annotation { + } + } + package test.pkg { + public abstract class Foo implements java.lang.annotation.Annotation { + } + } + """ + ) + } + + @Test + fun `Annotation class extraction, non-compat mode`() { + @Suppress("ConstantConditionIf") + if (SKIP_NON_COMPAT) { + println("Skipping test for non-compatibility mode which isn't fully done yet") + return + } + + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public @interface Foo { + String value(); + } + """ + ), + java( + """ + package android.annotation; + import static java.lang.annotation.ElementType.*; + import java.lang.annotation.*; + @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) + @Retention(RetentionPolicy.CLASS) + @SuppressWarnings("ALL") + public @interface SuppressLint { + String[] value(); + } + """ + ) + ), + compatibilityMode = false, + api = """ + package android.annotation { + public @interface SuppressLint { + method public abstract String[]! value(); + } + } + package test.pkg { + public @interface Foo { + method public abstract String! value(); + } + } + """ + ) + } + + @Test + fun `Superclass signature extraction`() { + // Make sure superclass statement is correct; inherited method from parent that has same + // signature isn't included in the child + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class Foo extends Super { + @Override public void base() { } + public void child() { } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class Super { + public void base() { } + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo extends test.pkg.Super { + ctor public Foo(); + method public void child(); + } + public class Super { + ctor public Super(); + method public void base(); + } + } + """ + ) + } + + @Test + fun `Extract fields with types and initial values`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class Foo { + private int hidden = 1; + int hidden2 = 2; + /** @hide */ + int hidden3 = 3; + + protected int field00; // No value + public static final boolean field01 = true; + public static final int field02 = 42; + public static final long field03 = 42L; + public static final short field04 = 5; + public static final byte field05 = 5; + public static final char field06 = 'c'; + public static final float field07 = 98.5f; + public static final double field08 = 98.5; + public static final String field09 = "String with \"escapes\" and \u00a9..."; + public static final double field10 = Double.NaN; + public static final double field11 = Double.POSITIVE_INFINITY; + + public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef"; + public static final char HEX_INPUT = 61184; + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(); + field public static final java.lang.String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef"; + field public static final char HEX_INPUT = 61184; // 0xef00 '\uef00' + field protected int field00; + field public static final boolean field01 = true; + field public static final int field02 = 42; // 0x2a + field public static final long field03 = 42L; // 0x2aL + field public static final short field04 = 5; // 0x5 + field public static final byte field05 = 5; // 0x5 + field public static final char field06 = 99; // 0x0063 'c' + field public static final float field07 = 98.5f; + field public static final double field08 = 98.5; + field public static final java.lang.String field09 = "String with \"escapes\" and \u00a9..."; + field public static final double field10 = (0.0/0.0); + field public static final double field11 = (1.0/0.0); + } + } + """ + ) + } + + @Test + fun `Check all modifiers`() { + // Include as many modifiers as possible to see which ones are included + // in the signature files, and the expected sorting order. + // Note that the signature files treat "deprecated" as a fake modifier. + // Note also how the "protected" modifier on the interface method gets + // promoted to public. + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public abstract class Foo { + @Deprecated private static final long field1 = 5; + @Deprecated private static volatile long field2 = 5; + @Deprecated public static strictfp final synchronized void method1() { } + @Deprecated public static final synchronized native void method2(); + @Deprecated protected static final class Inner1 { } + @Deprecated protected static abstract class Inner2 { } + @Deprecated protected interface Inner3 { + default void method3() { } + static void method4(final int arg) { } + } + } + """ + ) + ), + + warnings = """ + src/test/pkg/Foo.java:4: warning: Class test.pkg.Foo.Inner3: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch:113] + src/test/pkg/Foo.java:5: warning: Class test.pkg.Foo.Inner2: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch:113] + src/test/pkg/Foo.java:6: warning: Class test.pkg.Foo.Inner1: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch:113] + src/test/pkg/Foo.java:7: warning: Method test.pkg.Foo.method2(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch:113] + src/test/pkg/Foo.java:8: warning: Method test.pkg.Foo.method1(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch:113] + """, + + api = """ + package test.pkg { + public abstract class Foo { + ctor public Foo(); + method public static final deprecated synchronized void method1(); + method public static final deprecated synchronized void method2(); + } + protected static final deprecated class Foo.Inner1 { + ctor protected Foo.Inner1(); + } + protected static abstract deprecated class Foo.Inner2 { + ctor protected Foo.Inner2(); + } + protected static abstract deprecated interface Foo.Inner3 { + method public default void method3(); + method public static void method4(int); + } + } + """ + ) + } + + @Test + fun `Check all modifiers, non-compat mode`() { + @Suppress("ConstantConditionIf") + if (SKIP_NON_COMPAT) { + @Suppress("ConstantConditionIf") + println("Skipping test for non-compatibility mode which isn't fully done yet") + return + } + + // Like testModifiers but turns off compat mode, such that we have + // a modifier order more in line with standard code conventions + check( + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public abstract class Foo { + @Deprecated private static final long field1 = 5; + @Deprecated private static volatile long field2 = 5; + /** @deprecated */ @Deprecated public static strictfp final synchronized void method1() { } + /** @deprecated */ @Deprecated public static final synchronized native void method2(); + /** @deprecated */ @Deprecated protected static final class Inner1 { } + /** @deprecated */ @Deprecated protected static abstract class Inner2 { } + /** @deprecated */ @Deprecated protected interface Inner3 { + protected default void method3() { } + static void method4(final int arg) { } + } + } + """ + ) + ), + api = """ + package test.pkg { + public abstract class Foo { + ctor public Foo(); + method deprecated public static final synchronized strictfp void method1(); + method deprecated public static final synchronized native void method2(); + } + deprecated protected static final class Foo.Inner1 { + ctor protected Foo.Inner1(); + } + deprecated protected abstract static class Foo.Inner2 { + ctor protected Foo.Inner2(); + } + deprecated protected static interface Foo.Inner3 { + method public default void method3(); + method public static void method4(int); + } + } + """ + ) + } + + @Test + fun `Package with only hidden classes should be removed from signature files`() { + // Checks that if we have packages that are hidden, or contain only hidden or doconly + // classes, the entire package is omitted from the signature file. Note how the test.pkg1.sub + // package is not marked @hide, but doclava now treats subpackages of a hidden package + // as also hidden. + check( + sourceFiles = *arrayOf( + java( + """ + ${"/** @hide hidden package */" /* avoid dangling javadoc warning */} + package test.pkg1; + """ + ), + java( + """ + package test.pkg1; + @SuppressWarnings("ALL") + public class Foo { + // Hidden by package hide + } + """ + ), + java( + """ + package test.pkg2; + /** @hide hidden class in this package */ + @SuppressWarnings("ALL") + public class Bar { + } + """ + ), + java( + """ + package test.pkg2; + /** @doconly hidden class in this package */ + @SuppressWarnings("ALL") + public class Baz { + } + """ + ), + java( + """ + package test.pkg1.sub; + // Hidden by @hide in package above + @SuppressWarnings("ALL") + public class Test { + } + """ + ), + java( + """ + package test.pkg3; + // The only really visible class + @SuppressWarnings("ALL") + public class Boo { + } + """ + ) + ), + api = """ + package test.pkg3 { + public class Boo { + ctor public Boo(); + } + } + """ + ) + } + + @Test + fun `Enums can be abstract`() { + // As per https://bugs.openjdk.java.net/browse/JDK-6287639 + // abstract methods in enums should not be listed as abstract, + // but doclava1 does, so replicate this. + // Also checks that we handle both enum fields and regular fields + // and that they are listed separately. + + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public enum FooBar { + ABC { + @Override + protected void foo() { } + }, DEF { + @Override + protected void foo() { } + }; + + protected abstract void foo(); + public static int field1 = 1; + public int field2 = 2; + } + """ + ) + ), + api = """ + package test.pkg { + public class FooBar extends java.lang.Enum { + method protected abstract void foo(); + method public static test.pkg.FooBar valueOf(java.lang.String); + method public static final test.pkg.FooBar[] values(); + enum_constant public static final test.pkg.FooBar ABC; + enum_constant public static final test.pkg.FooBar DEF; + field public static int field1; + field public int field2; + } + } + """ + ) + } + + @Test + fun `Check erasure in throws-list`() { + // Makes sure that when we have a generic signature in the throws list we take + // the erasure instead (in compat mode); "Throwable" instead of "X" in the below + // test. Real world example: Optional.orElseThrow. + check( + compatibilityMode = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import java.util.function.Supplier; + + @SuppressWarnings("ALL") + public final class Test<T> { + public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X { + return null; + } + } + """ + ) + ), + api = """ + package test.pkg { + public final class Test<T> { + ctor public Test(); + method public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X>) throws java.lang.Throwable; + } + } + """ + ) + } + + @Test + fun `Check various generics signature subtleties`() { + // Some additional declarations where PSI default type handling diffs from doclava1 + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public abstract class Collections { + public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T> collection) { + return null; + } + public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T t); + public final class Range<T extends java.lang.Comparable<? super T>> { } + } + """ + ), java( + """ + package test.pkg; + + import java.util.Set; + + @SuppressWarnings("ALL") + public class MoreAsserts { + public static void assertEquals(String arg0, Set<? extends Object> arg1, Set<? extends Object> arg2) { } + public static void assertEquals(Set<? extends Object> arg1, Set<? extends Object> arg2) { } + } + + """ + ) + ), + + // This is the output from doclava1; I'm not quite matching this yet (sorting order differs, + // and my heuristic to remove "extends java.lang.Object" is somehow preserved here. I'm + // not clear on when they do it and when they don't. + /* + api = """ + package test.pkg { + public abstract class Collections { + ctor public Collections(); + method public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T); + method public static <T & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + } + public final class Collections.Range<T extends java.lang.Comparable<? super T>> { + ctor public Collections.Range(); + } + public class MoreAsserts { + ctor public MoreAsserts(); + method public static void assertEquals(java.util.Set<? extends java.lang.Object>, java.util.Set<? extends java.lang.Object>); + method public static void assertEquals(java.lang.String, java.util.Set<? extends java.lang.Object>, java.util.Set<? extends java.lang.Object>); + } + } + """, + */ + api = """ + package test.pkg { + public abstract class Collections { + ctor public Collections(); + method public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T); + method public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + } + public final class Collections.Range<T extends java.lang.Comparable<? super T>> { + ctor public Collections.Range(); + } + public class MoreAsserts { + ctor public MoreAsserts(); + method public static void assertEquals(java.lang.String, java.util.Set<?>, java.util.Set<?>); + method public static void assertEquals(java.util.Set<?>, java.util.Set<?>); + } + } + """, + + // Can't check doclava1 on this: its output doesn't match javac, e.g. for the above declaration + // of max, javap shows this signature: + // public static <T extends java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + // which matches metalava's output: + // method public static <T & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + // and not doclava1: + // method public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + + checkDoclava1 = false + ) + } + + @Test + fun `Check instance methods in enums`() { + // Make sure that when we have instance methods in an enum they're handled + // correctly (there's some special casing around enums to insert extra methods + // that was broken, as exposed by ChronoUnit#toString) + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public interface TempUnit { + @Override + String toString(); + } + """ + ), + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public enum ChronUnit implements TempUnit { + C, B, A; + + public String valueOf(int x) { + return Integer.toString(x + 5); + } + + public String values(String separator) { + return null; + } + + @Override + public String toString() { + return name(); + } + } + """ + ) + ), + importedPackages = emptyList(), + api = """ + package test.pkg { + public final class ChronUnit extends java.lang.Enum implements test.pkg.TempUnit { + method public static test.pkg.ChronUnit valueOf(java.lang.String); + method public java.lang.String valueOf(int); + method public static final test.pkg.ChronUnit[] values(); + method public final java.lang.String values(java.lang.String); + enum_constant public static final test.pkg.ChronUnit A; + enum_constant public static final test.pkg.ChronUnit B; + enum_constant public static final test.pkg.ChronUnit C; + } + public abstract interface TempUnit { + method public abstract java.lang.String toString(); + } + } + """ + ) + } + + @Test + fun `Mixing enums and fields`() { + // Checks sorting order of enum constant values + val source = """ + package java.nio.file.attribute { + public final class AclEntryPermission extends java.lang.Enum { + method public static java.nio.file.attribute.AclEntryPermission valueOf(java.lang.String); + method public static final java.nio.file.attribute.AclEntryPermission[] values(); + enum_constant public static final java.nio.file.attribute.AclEntryPermission APPEND_DATA; + enum_constant public static final java.nio.file.attribute.AclEntryPermission DELETE; + enum_constant public static final java.nio.file.attribute.AclEntryPermission DELETE_CHILD; + enum_constant public static final java.nio.file.attribute.AclEntryPermission EXECUTE; + enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_ACL; + enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_ATTRIBUTES; + enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_DATA; + enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_NAMED_ATTRS; + enum_constant public static final java.nio.file.attribute.AclEntryPermission SYNCHRONIZE; + enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_ACL; + enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_ATTRIBUTES; + enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_DATA; + enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_NAMED_ATTRS; + enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_OWNER; + field public static final java.nio.file.attribute.AclEntryPermission ADD_FILE; + field public static final java.nio.file.attribute.AclEntryPermission ADD_SUBDIRECTORY; + field public static final java.nio.file.attribute.AclEntryPermission LIST_DIRECTORY; + } + } + """ + check( + signatureSource = source, + api = source + ) + } + + @Test + fun `Superclass filtering, should skip intermediate hidden classes`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class MyClass extends HiddenParent { + public void method4() { } + } + """ + ), + java( + """ + package test.pkg; + /** @hide */ + @SuppressWarnings("ALL") + public class HiddenParent extends HiddenParent2 { + public static final String CONSTANT = "MyConstant"; + protected int mContext; + public void method3() { } + } + """ + ), + java( + """ + package test.pkg; + /** @hide */ + @SuppressWarnings("ALL") + public class HiddenParent2 extends PublicParent { + public void method2() { } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class PublicParent { + public void method1() { } + } + """ + ) + ), + // Notice how the intermediate methods (method2, method3) have been removed + includeStrippedSuperclassWarnings = true, + warnings = "src/test/pkg/MyClass.java:3: warning: Public class test.pkg.MyClass stripped of unavailable superclass test.pkg.HiddenParent [HiddenSuperclass:111]", + api = """ + package test.pkg { + public class MyClass extends test.pkg.PublicParent { + ctor public MyClass(); + method public void method4(); + } + public class PublicParent { + ctor public PublicParent(); + method public void method1(); + } + } + """ + ) + } + + @Test + fun `Inheriting from package private classes, package private class should be included`() { + check( + checkDoclava1 = true, + 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(); + } + } + """ + ) + } + + @Test + fun `When implementing rather than extending package private class, inline members instead`() { + // If you implement a package private interface, we just remove it and inline the members into + // the subclass + check( + compatibilityMode = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyClass implements HiddenInterface { + @Override public void method() { } + @Override public void other() { } + } + """ + ), + java( + """ + package test.pkg; + public interface OtherInterface { + void other(); + } + """ + ), + java( + """ + package test.pkg; + interface HiddenInterface extends OtherInterface { + void method() { } + String CONSTANT = "MyConstant"; + } + """ + ) + ), + api = """ + package test.pkg { + public class MyClass implements test.pkg.OtherInterface { + ctor public MyClass(); + method public void method(); + method public void other(); + field public static final java.lang.String CONSTANT = "MyConstant"; + } + public abstract interface OtherInterface { + method public abstract void other(); + } + } + """ + ) + } + + @Test + fun `Implementing package private class, non-compat mode`() { + @Suppress("ConstantConditionIf") + if (SKIP_NON_COMPAT) { + println("Skipping test for non-compatibility mode which isn't fully done yet") + return + } + + // Like the previous test, but in non compat mode we correctly + // include all the non-hidden public interfaces into the signature + + // BUG: Note that we need to implement the parent + check( + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyClass implements HiddenInterface { + @Override public void method() { } + @Override public void other() { } + } + """ + ), + java( + """ + package test.pkg; + public interface OtherInterface { + void other(); + } + """ + ), + java( + """ + package test.pkg; + interface HiddenInterface extends OtherInterface { + void method() { } + String CONSTANT = "MyConstant"; + } + """ + ) + ), + api = """ + package test.pkg { + public class MyClass implements test.pkg.OtherInterface { + ctor public MyClass(); + method public void method(); + method public void other(); + field public static final String! CONSTANT = "MyConstant"; + } + public interface OtherInterface { + method public void other(); + } + } + """ + ) + } + + @Test + fun `Default modifiers should be omitted`() { + // If signatures vary only by the "default" modifier in the interface, don't show it on the implementing + // class + check( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + public class MyClass implements SuperInterface { + @Override public void method() { } + @Override public void method2() { } + } + """ + ), + java( + """ + package test.pkg; + + public interface SuperInterface { + void method(); + default void method2() { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class MyClass implements test.pkg.SuperInterface { + ctor public MyClass(); + method public void method(); + } + public abstract interface SuperInterface { + method public abstract void method(); + method public default void method2(); + } + } + """ + ) + } + + @Test + fun `Override via different throws list should be included`() { + // If a method overrides another but changes the throws list, the overriding + // method must be listed in the subclass. This is observed for example in + // AbstractCursor#finalize, which omits the throws clause from Object's finalize. + check( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + public abstract class AbstractCursor extends Parent { + @Override protected void finalize2() { } // note: not throws Throwable! + } + """ + ), + java( + """ + package test.pkg; + + @SuppressWarnings("RedundantThrows") + public class Parent { + protected void finalize2() throws Throwable { + } + } + """ + ) + ), + api = """ + package test.pkg { + public abstract class AbstractCursor extends test.pkg.Parent { + ctor public AbstractCursor(); + method protected void finalize2(); + } + public class Parent { + ctor public Parent(); + method protected void finalize2() throws java.lang.Throwable; + } + } + """ + ) + } + + @Test + fun `Implementing interface method`() { + // If you have a public method that implements an interface method, + // they'll vary in the "abstract" modifier, but it shouldn't be listed on the + // class. This is an issue for example for the ZonedDateTime#getLong method + // implementing the TemporalAccessor#getLong method + check( + sourceFiles = *arrayOf( +// java( +// """ +// package test.pkg; +// public interface SomeInterface { +// long getLong(); +// } +// """ +// ), + java( + """ + package test.pkg; + public interface SomeInterface2 { + @Override default long getLong() { + return 42; + } + } + """ + ), + java( + """ + package test.pkg; + public class Foo implements /*SomeInterface,*/ SomeInterface2 { + @Override + public long getLong() { return 0L; } + } + """ + ) + ), +// api = """ +// package test.pkg { +// public class Foo implements test.pkg.SomeInterface test.pkg.SomeInterface2 { +// ctor public Foo(); +// } +// public abstract interface SomeInterface { +// method public abstract long getLong(); +// } +// public abstract interface SomeInterface2 { +// method public default long getLong(); +// } +// } +// """ + api = """ + package test.pkg { + public class Foo implements test.pkg.SomeInterface2 { + ctor public Foo(); + } + public abstract interface SomeInterface2 { + method public default long getLong(); + } + } + """ + ) + } + + @Test + fun `Check basic @remove scenarios`() { + // Test basic @remove handling for methods and fields + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("JavaDoc") + public class Bar { + /** @removed */ + public Bar() { } + public int field; + public void test() { } + /** @removed */ + public int removedField; + /** @removed */ + public void removedMethod() { } + /** @removed and @hide - should not be listed */ + public int hiddenField; + + /** @removed */ + public class Inner { } + + public class Inner2 { + public class Inner3 { + /** @removed */ + public class Inner4 { } + } + } + + public class Inner5 { + public class Inner6 { + public class Inner7 { + /** @removed */ + public int removed; + } + } + } + } + """ + ) + ), + removedApi = """ + package test.pkg { + public class Bar { + ctor public Bar(); + method public void removedMethod(); + field public int removedField; + } + public class Bar.Inner { + ctor public Bar.Inner(); + } + public class Bar.Inner2.Inner3.Inner4 { + ctor public Bar.Inner2.Inner3.Inner4(); + } + public class Bar.Inner5.Inner6.Inner7 { + field public int removed; + } + } + """ + ) + } + + @Test + fun `Check @remove class`() { + // Test removing classes + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + /** @removed */ + @SuppressWarnings("JavaDoc") + public class Foo { + public void foo() { } + public class Inner { + } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("JavaDoc") + public class Bar implements Parcelable { + public int field; + public void method(); + + /** @removed */ + public int removedField; + /** @removed */ + public void removedMethod() { } + + public class Inner1 { + } + /** @removed */ + public class Inner2 { + } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface Parcelable { + void method(); + } + """ + ) + ), + /* + I expected this: but doclava1 doesn't do that (and we now match its behavior) + package test.pkg { + public class Bar { + method public void removedMethod(); + field public int removedField; + } + public class Bar.Inner2 { + } + public class Foo { + method public void foo(); + } + } + */ + removedApi = """ + package test.pkg { + public class Bar implements test.pkg.Parcelable { + method public void removedMethod(); + field public int removedField; + } + public class Bar.Inner2 { + ctor public Bar.Inner2(); + } + public class Foo { + ctor public Foo(); + method public void foo(); + } + public class Foo.Inner { + ctor public Foo.Inner(); + } + } + """ + ) + } + + @Test + fun `Test include overridden @Deprecated even if annotated with @hide`() { + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("JavaDoc") + public class Child extends Parent { + /** + * @deprecated + * @hide + */ + @Deprecated @Override + public String toString() { + return "Child"; + } + } + """ + ), + java( + """ + package test.pkg; + public class Parent { + public String toString() { + return "Parent"; + } + } + """ + ) + ), + api = """ + package test.pkg { + public class Child extends test.pkg.Parent { + ctor public Child(); + method public deprecated java.lang.String toString(); + } + public class Parent { + ctor public Parent(); + } + } + """ + ) + } + + @Test + fun `Indirect Field Includes from Interfaces`() { + // Real-world example: include ZipConstants into ZipFile and JarFile + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg1; + interface MyConstants { + long CONSTANT1 = 12345; + long CONSTANT2 = 67890; + long CONSTANT3 = 42; + } + """ + ), + java( + """ + package test.pkg1; + import java.io.Closeable; + @SuppressWarnings("WeakerAccess") + public class MyParent implements MyConstants, Closeable { + } + """ + ), + java( + """ + package test.pkg2; + + import test.pkg1.MyParent; + public class MyChild extends MyParent { + } + """ + ) + + ), + api = """ + package test.pkg1 { + public class MyParent implements java.io.Closeable { + ctor public MyParent(); + field public static final long CONSTANT1 = 12345L; // 0x3039L + field public static final long CONSTANT2 = 67890L; // 0x10932L + field public static final long CONSTANT3 = 42L; // 0x2aL + } + } + package test.pkg2 { + public class MyChild extends test.pkg1.MyParent { + ctor public MyChild(); + field public static final long CONSTANT1 = 12345L; // 0x3039L + field public static final long CONSTANT2 = 67890L; // 0x10932L + field public static final long CONSTANT3 = 42L; // 0x2aL + } + } + """ + ) + } + + @Test + fun `Skip interfaces from packages explicitly hidden via arguments`() { + // Real-world example: HttpResponseCache implements OkCacheContainer but hides the only inherited method + check( + checkDoclava1 = true, + extraArguments = arrayOf( + "--hide-package", "com.squareup.okhttp" + ), + sourceFiles = *arrayOf( + java( + """ + package android.net.http; + import com.squareup.okhttp.Cache; + import com.squareup.okhttp.OkCacheContainer; + import java.io.Closeable; + import java.net.ResponseCache; + @SuppressWarnings("JavaDoc") + public final class HttpResponseCache implements Closeable, OkCacheContainer { + /** @hide Needed for OkHttp integration. */ + @Override + public Cache getCache() { + return delegate.getCache(); + } + } + """ + ), + java( + """ + package com.squareup.okhttp; + public interface OkCacheContainer { + Cache getCache(); + } + """ + ) + ), + api = """ + package android.net.http { + public final class HttpResponseCache implements java.io.Closeable { + ctor public HttpResponseCache(); + } + } + """ + ) + } + + @Test + fun `Private API signatures`() { + check( + checkDoclava1 = false, // doclava1 doesn't have the same behavior: see + // https://android-review.googlesource.com/c/platform/external/doclava/+/589515 + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Class1 { + Class1(int arg) { } + /** @hide */ + public void method1() { } + void method2() { } + private void method3() { } + public int field1 = 1; + protected int field2 = 2; + int field3 = 3; + float[][] field4 = 3; + long[] field5 = null; + private int field6 = 4; + void myVarargsMethod(int x, String... args) { } + + public class Inner { // Fully public, should not be included + public void publicMethod() { } + } + } + """ + ), + + java( + """ + package test.pkg; + class Class2 { + public void method4() { } + + private class Class3 { + public void method5() { } + } + } + """ + ), + + java( + """ + package test.pkg; + /** @doconly */ + class Class4 { + public void method5() { } + } + """ + ) + ), + privateApi = """ + package test.pkg { + public class Class1 { + ctor Class1(int); + method public void method1(); + method void method2(); + method private void method3(); + method void myVarargsMethod(int, java.lang.String...); + field int field3; + field float[][] field4; + field long[] field5; + field private int field6; + } + class Class2 { + ctor Class2(); + method public void method4(); + } + private class Class2.Class3 { + ctor private Class2.Class3(); + method public void method5(); + } + class Class4 { + ctor Class4(); + method public void method5(); + } + } + """, + privateDexApi = """ + Ltest/pkg/Class1;-><init>(I)V + Ltest/pkg/Class1;->method1()V + Ltest/pkg/Class1;->method2()V + Ltest/pkg/Class1;->method3()V + Ltest/pkg/Class1;->myVarargsMethod(I[Ljava/lang/String;)V + Ltest/pkg/Class1;->field3:I + Ltest/pkg/Class1;->field4:[[F + Ltest/pkg/Class1;->field5:[J + Ltest/pkg/Class1;->field6:I + Ltest/pkg/Class2; + Ltest/pkg/Class2;-><init>()V + Ltest/pkg/Class2;->method4()V + Ltest/pkg/Class2${"$"}Class3; + Ltest/pkg/Class2${"$"}Class3;-><init>()V + Ltest/pkg/Class2${"$"}Class3;->method5()V + Ltest/pkg/Class4; + Ltest/pkg/Class4;-><init>()V + Ltest/pkg/Class4;->method5()V + """ + ) + } + + @Test + fun `Extend from multiple interfaces`() { + // Real-world example: XmlResourceParser + check( + checkDoclava1 = true, + checkCompilation = true, + sourceFiles = *arrayOf( + java( + """ + package android.content.res; + import android.util.AttributeSet; + import org.xmlpull.v1.XmlPullParser; + import my.AutoCloseable; + + @SuppressWarnings("UnnecessaryInterfaceModifier") + public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { + public void close(); + } + """ + ), + java( + """ + package android.util; + @SuppressWarnings("WeakerAccess") + public interface AttributeSet { + } + """ + ), + java( + """ + package my; + public interface AutoCloseable { + } + """ + ), + java( + """ + package org.xmlpull.v1; + @SuppressWarnings("WeakerAccess") + public interface XmlPullParser { + } + """ + ) + ), + api = """ + package android.content.res { + public abstract interface XmlResourceParser implements android.util.AttributeSet my.AutoCloseable org.xmlpull.v1.XmlPullParser { + method public abstract void close(); + } + } + package android.util { + public abstract interface AttributeSet { + } + } + package my { + public abstract interface AutoCloseable { + } + } + package org.xmlpull.v1 { + public abstract interface XmlPullParser { + } + } + """ + ) + } + + @Test + fun `Including private interfaces from types`() { + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java("""package test.pkg1; interface Interface1 { }"""), + java("""package test.pkg1; abstract class Class1 { }"""), + java("""package test.pkg1; abstract class Class2 { }"""), + java("""package test.pkg1; abstract class Class3 { }"""), + java("""package test.pkg1; abstract class Class4 { }"""), + java("""package test.pkg1; abstract class Class5 { }"""), + java("""package test.pkg1; abstract class Class6 { }"""), + java("""package test.pkg1; abstract class Class7 { }"""), + java("""package test.pkg1; abstract class Class8 { }"""), + java("""package test.pkg1; abstract class Class9 { }"""), + java( + """ + package test.pkg1; + + import java.util.List; + import java.util.Map; + public abstract class Usage implements List<Class1> { + <T extends java.lang.Comparable<? super T>> void sort(java.util.List<T> list) {} + public Class3 myClass1 = null; + public List<? extends Class4> myClass2 = null; + public Map<String, ? extends Class5> myClass3 = null; + public <T extends Class6> void mySort(List<Class7> list, T element) {} + public void ellipsisType(Class8... myargs); + public void arrayType(Class9[] myargs); + } + """ + ) + ), + + // TODO: Test annotations! (values, annotation classes, etc.) + warnings = """ + src/test/pkg1/Usage.java:1: warning: Parameter myargs references hidden type class test.pkg1.Class9. [HiddenTypeParameter:121] + src/test/pkg1/Usage.java:2: warning: Parameter myargs references hidden type class test.pkg1.Class8. [HiddenTypeParameter:121] + src/test/pkg1/Usage.java:3: warning: Parameter list references hidden type class test.pkg1.Class7. [HiddenTypeParameter:121] + src/test/pkg1/Usage.java:4: warning: Field Usage.myClass3 references hidden type class test.pkg1.Class5. [HiddenTypeParameter:121] + src/test/pkg1/Usage.java:5: warning: Field Usage.myClass2 references hidden type class test.pkg1.Class4. [HiddenTypeParameter:121] + src/test/pkg1/Usage.java:6: warning: Field Usage.myClass1 references hidden type test.pkg1.Class3. [HiddenTypeParameter:121] + """, + api = """ + package test.pkg1 { + public abstract class Usage implements java.util.List { + ctor public Usage(); + method public void arrayType(test.pkg1.Class9[]); + method public void ellipsisType(test.pkg1.Class8...); + method public <T extends test.pkg1.Class6> void mySort(java.util.List<test.pkg1.Class7>, T); + field public test.pkg1.Class3 myClass1; + field public java.util.List<? extends test.pkg1.Class4> myClass2; + field public java.util.Map<java.lang.String, ? extends test.pkg1.Class5> myClass3; + } + } + """ + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt b/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt new file mode 100644 index 0000000..880376e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2017 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 org.intellij.lang.annotations.Language +import org.junit.Test + +class ApiFromTextTest : DriverTest() { + + @Test + fun `Loading a signature file and writing the API back out`() { + val source = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public int clamp(int); + method public java.lang.Double convert(java.lang.Float); + field public static final java.lang.String ANY_CURSOR_ITEM_TYPE = "vnd.android.cursor.item/*"; + field public java.lang.Number myNumber; + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = source + ) + } + + @Test + fun `Test generics, superclasses and interfaces`() { + val source = """ + package a.b.c { + public abstract interface MyStream<T, S extends a.b.c.MyStream<T, S>> { + } + } + package test.pkg { + public final class Foo extends java.lang.Enum { + ctor public Foo(int); + ctor public Foo(int, int); + method public static test.pkg.Foo valueOf(java.lang.String); + method public static final test.pkg.Foo[] values(); + enum_constant public static final test.pkg.Foo A; + enum_constant public static final test.pkg.Foo B; + } + public abstract interface MyBaseInterface { + } + public abstract interface MyInterface<T> implements test.pkg.MyBaseInterface { + } + public abstract interface MyInterface2<T extends java.lang.Number> implements test.pkg.MyBaseInterface { + } + public static abstract class MyInterface2.Range<T extends java.lang.Comparable<? super T>> { + ctor public MyInterface2.Range(); + } + public static class MyInterface2.TtsSpan<C extends test.pkg.MyInterface<?>> { + ctor public MyInterface2.TtsSpan(); + } + public final class Test<T> { + ctor public Test(); + method public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T); + method public static <T & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>); + method public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X>) throws java.lang.Throwable; + field public static java.util.List<java.lang.String> LIST; + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = source + ) + } + + @Test + fun `Test constants`() { + val source = """ + package test.pkg { + public class Foo2 { + ctor public Foo2(); + field public static final java.lang.String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef"; + field public static final char HEX_INPUT = 61184; // 0xef00 '\uef00' + field protected int field00; + field public static final boolean field01 = true; + field public static final int field02 = 42; // 0x2a + field public static final long field03 = 42L; // 0x2aL + field public static final short field04 = 5; // 0x5 + field public static final byte field05 = 5; // 0x5 + field public static final char field06 = 99; // 0x0063 'c' + field public static final float field07 = 98.5f; + field public static final double field08 = 98.5; + field public static final java.lang.String field09 = "String with \"escapes\" and \u00a9..."; + field public static final double field10 = (0.0/0.0); + field public static final double field11 = (1.0/0.0); + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = source + ) + } + + @Test + fun `Test inner classes`() { + val source = """ + package test.pkg { + public abstract class Foo { + ctor public Foo(); + method public static final deprecated synchronized void method1(); + method public static final deprecated synchronized void method2(); + } + protected static final deprecated class Foo.Inner1 { + ctor protected Foo.Inner1(); + } + protected static abstract deprecated class Foo.Inner2 { + ctor protected Foo.Inner2(); + } + protected static abstract deprecated interface Foo.Inner3 { + method public default void method3(); + method public static void method4(int); + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = source + ) + } + + @Test + fun `Test throws`() { + val source = """ + package test.pkg { + public final class Test<T> { + ctor public Test(); + method public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X>) throws java.lang.Throwable; + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = source + ) + } + + @Test + fun `Loading a signature file with annotations on classes, fields, methods and parameters`() { + @Language("TEXT") + val source = """ + package test.pkg { + @android.support.annotation.UiThread public class MyTest { + ctor public MyTest(); + method @android.support.annotation.IntRange(from=10, to=20) public int clamp(int); + method public java.lang.Double? convert(java.lang.Float myPublicName); + field public java.lang.Number? myNumber; + } + } + """ + + check( + compatibilityMode = false, + inputKotlinStyleNulls = true, + omitCommonPackages = false, + signatureSource = source, + api = source + ) + } + + @Test + fun `Enums and annotations`() { + // In non-compat mode we write interfaces out as "@interface" (instead of abstract class) + // and similarly for enums we write "enum" instead of "class extends java.lang.Enum". + // Make sure we can also read this back in. + val source = """ + package android.annotation { + public @interface SuppressLint { + method public abstract String[] value(); + } + } + package test.pkg { + public enum Foo { + enum_constant public static final test.pkg.Foo A; + enum_constant public static final test.pkg.Foo B; + } + } + """ + + check( + compatibilityMode = false, + outputKotlinStyleNulls = false, + signatureSource = source, + api = source + ) + } + + @Test + fun `Enums and annotations exported to compat`() { + val source = """ + package android.annotation { + public @interface SuppressLint { + } + } + package test.pkg { + public final enum Foo { + enum_constant public static final test.pkg.Foo A; + enum_constant public static final test.pkg.Foo B; + } + } + """ + + check( + compatibilityMode = true, + signatureSource = source, + api = """ + package android.annotation { + public abstract class SuppressLint implements java.lang.annotation.Annotation { + } + } + package test.pkg { + public final class Foo extends java.lang.Enum { + enum_constant public static final test.pkg.Foo A; + enum_constant public static final test.pkg.Foo B; + } + } + """ + ) + } + + @Test + fun `Sort throws list by full name`() { + check( + compatibilityMode = true, + signatureSource = """ + package android.accounts { + public abstract interface AccountManagerFuture<V> { + method public abstract boolean cancel(boolean); + method public abstract V getResult() throws android.accounts.OperationCanceledException, java.io.IOException, android.accounts.AuthenticatorException; + method public abstract V getResult(long, java.util.concurrent.TimeUnit) throws android.accounts.OperationCanceledException, java.io.IOException, android.accounts.AuthenticatorException; + method public abstract boolean isCancelled(); + method public abstract boolean isDone(); + } + } + """, + api = """ + package android.accounts { + public abstract interface AccountManagerFuture<V> { + method public abstract boolean cancel(boolean); + method public abstract V getResult() throws android.accounts.AuthenticatorException, java.io.IOException, android.accounts.OperationCanceledException; + method public abstract V getResult(long, java.util.concurrent.TimeUnit) throws android.accounts.AuthenticatorException, java.io.IOException, android.accounts.OperationCanceledException; + method public abstract boolean isCancelled(); + method public abstract boolean isDone(); + } + } + """ + ) + } + +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt new file mode 100644 index 0000000..fc95b52 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2017 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 org.junit.Test + +class +CompatibilityCheckTest : DriverTest() { + @Test + fun `Change between class and interface`() { + check( + checkCompatibility = true, + warnings = """ + TESTROOT/load-api.txt:2: error: Class test.pkg.MyTest1 changed class/interface declaration [ChangedClass:23] + TESTROOT/load-api.txt:4: error: Class test.pkg.MyTest2 changed class/interface declaration [ChangedClass:23] + """, + compatibilityMode = false, + previousApi = """ + package test.pkg { + public class MyTest1 { + } + public interface MyTest2 { + } + public class MyTest3 { + } + public interface MyTest4 { + } + } + """, + // MyTest1 and MyTest2 reversed from class to interface or vice versa, MyTest3 and MyTest4 unchanged + signatureSource = """ + package test.pkg { + public interface MyTest1 { + } + public class MyTest2 { + } + public class MyTest3 { + } + public interface MyTest4 { + } + } + """ + ) + } + + @Test + fun `Interfaces should not be dropped`() { + check( + checkCompatibility = true, + warnings = """ + TESTROOT/load-api.txt:2: error: Class test.pkg.MyTest1 changed class/interface declaration [ChangedClass:23] + TESTROOT/load-api.txt:4: error: Class test.pkg.MyTest2 changed class/interface declaration [ChangedClass:23] + """, + compatibilityMode = false, + previousApi = """ + package test.pkg { + public class MyTest1 { + } + public interface MyTest2 { + } + public class MyTest3 { + } + public interface MyTest4 { + } + } + """, + // MyTest1 and MyTest2 reversed from class to interface or vice versa, MyTest3 and MyTest4 unchanged + signatureSource = """ + package test.pkg { + public interface MyTest1 { + } + public class MyTest2 { + } + public class MyTest3 { + } + public interface MyTest4 { + } + } + """ + ) + } + + @Test + fun `Ensure warnings for removed APIs`() { + check( + checkCompatibility = true, + warnings = """ + TESTROOT/previous-api.txt:3: error: Removed method test.pkg.MyTest1.method [RemovedMethod:9] + TESTROOT/previous-api.txt:4: error: Removed field test.pkg.MyTest1.field [RemovedField:10] + TESTROOT/previous-api.txt:6: error: Removed class test.pkg.MyTest2 [RemovedClass:8] + """, + compatibilityMode = false, + previousApi = """ + package test.pkg { + public class MyTest1 { + method public Double method(Float); + field public Double field; + } + public class MyTest2 { + method public Double method(Float); + field public Double field; + } + } + package test.pkg.other { + } + """, + signatureSource = """ + package test.pkg { + public class MyTest1 { + } + } + """, + api = """ + package test.pkg { + public class MyTest1 { + } + } + """ + ) + } + + @Test + fun `Flag invalid nullness changes`() { + check( + checkCompatibility = true, + warnings = """ + TESTROOT/load-api.txt:5: error: Attempted to remove @Nullable annotation from method test.pkg.MyTest.convert3 [InvalidNullConversion:40] + TESTROOT/load-api.txt:5: error: Attempted to remove @Nullable annotation from parameter arg1 in test.pkg.MyTest.convert3 [InvalidNullConversion:40] + TESTROOT/load-api.txt:6: error: Attempted to remove @NonNull annotation from method test.pkg.MyTest.convert4 [InvalidNullConversion:40] + TESTROOT/load-api.txt:6: error: Attempted to remove @NonNull annotation from parameter arg1 in test.pkg.MyTest.convert4 [InvalidNullConversion:40] + TESTROOT/load-api.txt:7: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter arg1 in test.pkg.MyTest.convert5 [InvalidNullConversion:40] + TESTROOT/load-api.txt:8: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.MyTest.convert6 [InvalidNullConversion:40] + """, + compatibilityMode = false, + outputKotlinStyleNulls = false, + previousApi = """ + package test.pkg { + public class MyTest { + method public Double convert1(Float); + method public Double convert2(Float); + method @Nullable public Double convert3(@Nullable Float); + method @NonNull public Double convert4(@NonNull Float); + method @Nullable public Double convert5(@Nullable Float); + method @NonNull public Double convert6(@NonNull Float); + } + } + """, + // Changes: +nullness, -nullness, nullable->nonnull, nonnull->nullable + signatureSource = """ + package test.pkg { + public class MyTest { + method @Nullable public Double convert1(@Nullable Float); + method @NonNull public Double convert2(@NonNull Float); + method public Double convert3(Float); + method public Double convert4(Float); + method @NonNull public Double convert5(@NonNull Float); + method @Nullable public Double convert6(@Nullable Float); + } + } + """, + api = """ + package test.pkg { + public class MyTest { + method @Nullable public Double convert1(@Nullable Float); + method @NonNull public Double convert2(@NonNull Float); + method public Double convert3(Float); + method public Double convert4(Float); + method @NonNull public Double convert5(@NonNull Float); + method @Nullable public Double convert6(@Nullable Float); + } + } + """ + ) + } + + @Test + fun `Kotlin Nullness`() { + check( + checkCompatibility = true, + warnings = """ + src/test/pkg/Outer.kt:2: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.Inner.method3 [InvalidNullConversion:40] + src/test/pkg/Outer.kt:3: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.Outer.Inner.method2 [InvalidNullConversion:40] + src/test/pkg/Outer.kt:3: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.Inner.method2 [InvalidNullConversion:40] + src/test/pkg/Outer.kt:5: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.method3 [InvalidNullConversion:40] + src/test/pkg/Outer.kt:6: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.Outer.method2 [InvalidNullConversion:40] + src/test/pkg/Outer.kt:6: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.method2 [InvalidNullConversion:40] + """, + compatibilityMode = false, + inputKotlinStyleNulls = true, + outputKotlinStyleNulls = true, + previousApi = """ + package test.pkg { + public final class Outer { + ctor public Outer(); + method public final String? method1(String, String?); + method public final String method2(String?, String); + method public final String? method3(String, String?); + } + public static final class Outer.Inner { + ctor public Outer.Inner(); + method public final String method2(String?, String); + method public final String? method3(String, String?); + } + } + """, + sourceFiles = *arrayOf( + kotlin( + """ + package test.pkg + + class Outer { + fun method1(string: String, maybeString: String?): String? = null + fun method2(string: String, maybeString: String?): String? = null + fun method3(maybeString: String?, string : String): String = "" + class Inner { + fun method2(string: String, maybeString: String?): String? = null + fun method3(maybeString: String?, string : String): String = "" + } + } + """ + ) + ), + api = """ + package test.pkg { + public final class Outer { + ctor public Outer(); + method public final String? method1(String string, String? maybeString); + method public final String? method2(String string, String? maybeString); + method public final String method3(String? maybeString, String string); + } + public static final class Outer.Inner { + ctor public Outer.Inner(); + method public final String? method2(String string, String? maybeString); + method public final String method3(String? maybeString, String string); + } + } + """ + ) + } + + @Test + fun `Java Parameter Name Change`() { + check( + checkCompatibility = true, + warnings = """ + src/test/pkg/JavaClass.java:1: error: Attempted to change parameter name from secondParameter to newName in method test.pkg.JavaClass.method2 [ParameterNameChange:41] + src/test/pkg/JavaClass.java:2: error: Attempted to remove parameter name from parameter newName in test.pkg.JavaClass.method1 in method test.pkg.JavaClass.method1 [ParameterNameChange:41] + """, + compatibilityMode = false, + previousApi = """ + package test.pkg { + public class JavaClass { + ctor public JavaClass(); + method public String method1(String parameterName); + method public String method2(String firstParameter, String secondParameter); + } + } + """, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.ParameterName; + + public class JavaClass { + public String method1(String newName) { return null; } + public String method2(@ParameterName("firstParameter") String s, @ParameterName("newName") String prevName) { return null; } + } + """ + ), + supportParameterName + ), + api = """ + package test.pkg { + public class JavaClass { + ctor public JavaClass(); + method public String! method1(String!); + method public String! method2(String! firstParameter, String! newName); + } + } + """, + extraArguments = arrayOf("--hide-package", "android.support.annotation") + ) + } + + @Test + fun `Kotlin Parameter Name Change`() { + check( + checkCompatibility = true, + warnings = """ + src/test/pkg/KotlinClass.kt:1: error: Attempted to change parameter name from prevName to newName in method test.pkg.KotlinClass.method1 [ParameterNameChange:41] + """, + compatibilityMode = false, + inputKotlinStyleNulls = true, + outputKotlinStyleNulls = true, + previousApi = """ + package test.pkg { + public final class KotlinClass { + ctor public KotlinClass(); + method public final String? method1(String prevName); + } + } + """, + sourceFiles = *arrayOf( + kotlin( + """ + package test.pkg + + class KotlinClass { + fun method1(newName: String): String? = null + } + """ + ) + ), + api = """ + package test.pkg { + public final class KotlinClass { + ctor public KotlinClass(); + method public final String? method1(String newName); + } + } + """ + ) + } + + @Test + fun `Add flag new methods but not overrides from platform`() { + check( + checkCompatibility = true, + warnings = """ + src/test/pkg/MyClass.java:2: error: Added field test.pkg.MyClass.newField [AddedField:5] + src/test/pkg/MyClass.java:3: error: Added method test.pkg.MyClass.method2 [AddedMethod:4] + """, + compatibilityMode = false, + previousApi = """ + package test.pkg { + public class MyClass { + method public String method1(String); + } + } + """, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public class MyClass { + private MyClass() { } + public String method1(String newName) { return null; } + public String method2(String newName) { return null; } + public int newField = 5; + public String toString() { return "Hello World"; } + } + """ + ) + ) + ) + } + + // TODO: Check method signatures changing incompatibly (look especially out for adding new overloaded + // methods and comparator getting confused!) + // ..equals on the method items should actually be very useful! +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt new file mode 100644 index 0000000..49a3a02 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -0,0 +1,1030 @@ +package com.android.tools.metalava + +import com.android.tools.metalava.model.psi.trimDocIndent +import org.junit.Assert.assertEquals +import org.junit.Test + +/** Tests for the [DocAnalyzer] which enhances the docs */ +class DocAnalyzerTest : DriverTest() { + // TODO: Test @StringDef + + @Test + fun `Basic documentation generation test`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.Nullable; + import android.annotation.NonNull; + public class Foo { + /** These are the docs for method1. */ + @Nullable public Double method1(@NonNull Double factor1, @NonNull Double factor2) { } + /** These are the docs for method2. It can sometimes return null. */ + @Nullable public Double method2(@NonNull Double factor1, @NonNull Double factor2) { } + @Nullable public Double method3(@NonNull Double factor1, @NonNull Double factor2) { } + } + """ + ), + + nonNullSource, + nullableSource + ), + checkCompilation = false, // needs android.support annotations in classpath + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + /** + * These are the docs for method1. + * @param factor1 This value must never be {@code null}. + * @param factor2 This value must never be {@code null}. + * @return This value may be {@code null}. + */ + @android.support.annotation.Nullable public java.lang.Double method1(@android.support.annotation.NonNull java.lang.Double factor1, @android.support.annotation.NonNull java.lang.Double factor2) { throw new RuntimeException("Stub!"); } + /** + * These are the docs for method2. It can sometimes return null. + * @param factor1 This value must never be {@code null}. + * @param factor2 This value must never be {@code null}. + */ + @android.support.annotation.Nullable public java.lang.Double method2(@android.support.annotation.NonNull java.lang.Double factor1, @android.support.annotation.NonNull java.lang.Double factor2) { throw new RuntimeException("Stub!"); } + /** + * @param factor1 This value must never be {@code null}. + * @param factor2 This value must never be {@code null}. + * @return This value may be {@code null}. + */ + @android.support.annotation.Nullable public java.lang.Double method3(@android.support.annotation.NonNull java.lang.Double factor1, @android.support.annotation.NonNull java.lang.Double factor2) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Fix first sentence handling`() { + check( + sourceFiles = *arrayOf( + java( + """ + package android.annotation; + + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.CLASS; + import java.lang.annotation.*; + + /** + * Denotes that an integer parameter, field or method return value is expected + * to be a String resource reference (e.g. {@code android.R.string.ok}). + */ + @Documented + @Retention(CLASS) + @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE}) + public @interface StringRes { + } + """ + ) + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package android.annotation; + /** + * Denotes that an integer parameter, field or method return value is expected + * to be a String resource reference (e.g. {@code android.R.string.ok}). + */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public @interface StringRes { + } + """ + ) + ) + } + + @Test + fun `Fix typo replacement`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + /** This is an API for Andriod */ + public class Foo { + } + """ + ) + ), + checkCompilation = true, + checkDoclava1 = false, + warnings = "src/test/pkg/Foo.java:2: lint: Replaced Andriod with Android in documentation for class test.pkg.Foo [Typo:131]", + stubs = arrayOf( + """ + package test.pkg; + /** This is an API for Android */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Document Permissions`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.Manifest; + import android.annotation.RequiresPermission; + + public class PermissionTest { + @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + public void test1() { + } + + @RequiresPermission(allOf = Manifest.permission.ACCESS_COARSE_LOCATION) + public void test2() { + } + + @RequiresPermission(anyOf = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}) + public void test3() { + } + + @RequiresPermission(allOf = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCOUNT_MANAGER}) + public void test4() { + } + } + """ + ), + java( + """ + package android; + + public abstract class Manifest { + public static final class permission { + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; + public static final String ACCOUNT_MANAGER = "android.permission.ACCOUNT_MANAGER"; + } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = false, // needs android.support annotations in classpath + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class PermissionTest { + public PermissionTest() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} + */ + @android.support.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) public void test1() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} + */ + @android.support.annotation.RequiresPermission(allOf=android.Manifest.permission.ACCESS_COARSE_LOCATION) public void test2() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + */ + @android.support.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public void test3() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} and {@link android.Manifest.permission#ACCOUNT_MANAGER} + */ + @android.support.annotation.RequiresPermission(allOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCOUNT_MANAGER}) public void test4() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Document ranges`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.Manifest; + import android.annotation.IntRange; + + public class RangeTest { + @IntRange(from = 10) + public int test1(@IntRange(from = 20) int range2) { return 15; } + + @IntRange(from = 10, to = 20) + public int test2() { return 15; } + + @IntRange(to = 100) + public int test3() { return 50; } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * @param range2 Value is 20 or greater + * @return Value is 10 or greater + */ + @android.support.annotation.IntRange(from=10) public int test1(@android.support.annotation.IntRange(from=20) int range2) { throw new RuntimeException("Stub!"); } + /** + * @return Value is between 10 and 20 inclusive + */ + @android.support.annotation.IntRange(from=10, to=20) public int test2() { throw new RuntimeException("Stub!"); } + /** + * @return Value is 100 or less + */ + @android.support.annotation.IntRange(to=100) public int test3() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Merging in documentation snippets from annotation memberDoc and classDoc`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.UiThread; + import android.support.annotation.WorkerThread; + @UiThread + public class RangeTest { + @WorkerThread + public int test1() { } + } + """ + ), + uiThreadSource, + workerThreadSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + /** 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. * */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @android.support.annotation.UiThread public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** This method may take several seconds to complete, so it should + * only be called from a worker thread. */ + @android.support.annotation.WorkerThread public int test1() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Warn about multiple threading annotations`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.UiThread; + import android.support.annotation.WorkerThread; + public class RangeTest { + @UiThread @WorkerThread + public int test1() { } + } + """ + ), + uiThreadSource, + workerThreadSource + ), + checkCompilation = true, + checkDoclava1 = false, + warnings = "src/test/pkg/RangeTest.java:2: warning: Found more than one threading annotation on method test.pkg.RangeTest.test1(); the auto-doc feature does not handle this correctly [MultipleThreadAnnotations:133]", + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** This method must be called on the thread that originally created + * this UI element. This is typically the main thread of your app. + * This method may take several seconds to complete, so it should + * * only be called from a worker thread. + */ + @android.support.annotation.UiThread @android.support.annotation.WorkerThread public int test1() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Typedefs`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.annotation.IntDef; + import android.annotation.IntRange; + + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + + @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) + public class TypedefTest { + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + public static final int STYLE_NORMAL = 0; + public static final int STYLE_NO_TITLE = 1; + public static final int STYLE_NO_FRAME = 2; + public static final int STYLE_NO_INPUT = 3; + public static final int STYLE_UNRELATED = 3; + + public void setStyle(@DialogStyle int style, int theme) { + } + + @IntDef(value = {STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT, 2, 3 + 1}, + flag=true) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogFlags {} + + public void setFlags(Object first, @DialogFlags int flags) { + } + } + """ + ), + intRangeAnnotationSource, + intDefAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class TypedefTest { + public TypedefTest() { throw new RuntimeException("Stub!"); } + /** + * @param style Value is {@link test.pkg.TypedefTest#STYLE_NORMAL}, {@link test.pkg.TypedefTest#STYLE_NO_TITLE}, {@link test.pkg.TypedefTest#STYLE_NO_FRAME}, or {@link test.pkg.TypedefTest#STYLE_NO_INPUT} + */ + public void setStyle(int style, int theme) { throw new RuntimeException("Stub!"); } + /** + * @param flags Value is either <code>0</code> or a combination of {@link test.pkg.TypedefTest#STYLE_NORMAL}, {@link test.pkg.TypedefTest#STYLE_NO_TITLE}, {@link test.pkg.TypedefTest#STYLE_NO_FRAME}, {@link test.pkg.TypedefTest#STYLE_NO_INPUT}, 2, and 3 + 1 + */ + public void setFlags(java.lang.Object first, int flags) { throw new RuntimeException("Stub!"); } + public static final int STYLE_NORMAL = 0; // 0x0 + public static final int STYLE_NO_FRAME = 2; // 0x2 + public static final int STYLE_NO_INPUT = 3; // 0x3 + public static final int STYLE_NO_TITLE = 1; // 0x1 + public static final int STYLE_UNRELATED = 3; // 0x3 + } + """ + ) + ) + } + + @Test + fun `Typedefs combined with ranges`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.annotation.IntDef; + import android.annotation.IntRange; + + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + + @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) + public class TypedefTest { + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @IntRange(from = 20) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + public static final int STYLE_NORMAL = 0; + public static final int STYLE_NO_TITLE = 1; + public static final int STYLE_NO_FRAME = 2; + + public void setStyle(@DialogStyle int style, int theme) { + } + } + """ + ), + intRangeAnnotationSource, + intDefAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class TypedefTest { + public TypedefTest() { throw new RuntimeException("Stub!"); } + /** + * @param style Value is {@link test.pkg.TypedefTest#STYLE_NORMAL}, {@link test.pkg.TypedefTest#STYLE_NO_TITLE}, {@link test.pkg.TypedefTest#STYLE_NO_FRAME}, or STYLE_NO_INPUT + * Value is 20 or greater + */ + public void setStyle(int style, int theme) { throw new RuntimeException("Stub!"); } + public static final int STYLE_NORMAL = 0; // 0x0 + public static final int STYLE_NO_FRAME = 2; // 0x2 + public static final int STYLE_NO_TITLE = 1; // 0x1 + } + """ + ) + ) + } + + @Test + fun `Create method documentation from nothing`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + public class RangeTest { + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + @RequiresPermission(ACCESS_COARSE_LOCATION) + public void test1() { + } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION} + */ + @android.support.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION) public void test1() { throw new RuntimeException("Stub!"); } + public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + } + """ + ) + ) + } + + @Test + fun `Warn about missing field`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + public class RangeTest { + @RequiresPermission("MyPermission") + public void test1() { + } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = true, + checkDoclava1 = false, + warnings = "src/test/pkg/RangeTest.java:3: lint: Cannot find permission field for \"MyPermission\" required by method test.pkg.RangeTest.test1() (may be hidden or removed) [MissingPermission:132]", + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * Requires "MyPermission" + */ + @android.support.annotation.RequiresPermission("MyPermission") public void test1() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add to existing single-line method documentation`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + public class RangeTest { + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + /** This is the existing documentation. */ + @RequiresPermission(ACCESS_COARSE_LOCATION) + public int test1() { } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION} + */ + @android.support.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION) public int test1() { throw new RuntimeException("Stub!"); } + public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + } + """ + ) + ) + } + + @Test + fun `Add to existing multi-line method documentation`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + public class RangeTest { + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + /** + * This is the existing documentation. + * Multiple lines of it. + */ + @RequiresPermission(ACCESS_COARSE_LOCATION) + public int test1() { } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * Multiple lines of it. + * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION} + */ + @android.support.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION) public int test1() { throw new RuntimeException("Stub!"); } + public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + } + """ + ) + ) + } + + @Test + fun `Add new parameter when no doc exists`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + public int test1(int parameter1, @IntRange(from = 10) int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * @param parameter2 Value is 10 or greater + */ + public int test1(int parameter1, @android.support.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add to method when there are existing parameter docs and appear before these`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + public class RangeTest { + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + /** + * This is the existing documentation. + * @param parameter1 docs for parameter1 + * @param parameter2 docs for parameter2 + * @param parameter3 docs for parameter2 + * @return return value documented here + */ + @RequiresPermission(ACCESS_COARSE_LOCATION) + public int test1(int parameter1, int parameter2, int parameter3) { } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION} + * @param parameter1 docs for parameter1 + * @param parameter2 docs for parameter2 + * @param parameter3 docs for parameter2 + * @return return value documented here + */ + @android.support.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION) public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + } + """ + ) + ) + } + + @Test + fun `Add new parameter when doc exists but no param doc`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + /** + * This is the existing documentation. + * @return return value documented here + */ + public int test1(int parameter1, @IntRange(from = 10) int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * @param parameter2 Value is 10 or greater + * @return return value documented here + */ + public int test1(int parameter1, @android.support.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add new parameter, sorted correctly between existing ones`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + /** + * This is the existing documentation. + * @param parameter1 docs for parameter1 + * @param parameter3 docs for parameter2 + * @return return value documented here + */ + public int test1(int parameter1, @IntRange(from = 10) int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * @param parameter1 docs for parameter1 + * @param parameter3 docs for parameter2 + * @param parameter2 Value is 10 or greater + * @return return value documented here + */ + public int test1(int parameter1, @android.support.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add to existing parameter`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + /** + * This is the existing documentation. + * @param parameter1 docs for parameter1 + * @param parameter2 docs for parameter2 + * @param parameter3 docs for parameter2 + * @return return value documented here + */ + public int test1(int parameter1, @IntRange(from = 10) int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * @param parameter1 docs for parameter1 + * @param parameter2 docs for parameter2 + * Value is 10 or greater + * @param parameter3 docs for parameter2 + * @return return value documented here + */ + public int test1(int parameter1, @android.support.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add new return value`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + @IntRange(from = 10) + public int test1(int parameter1, int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * @return Value is 10 or greater + */ + @android.support.annotation.IntRange(from=10) public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Add to existing return value (ensuring it appears last)`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.IntRange; + public class RangeTest { + /** + * This is the existing documentation. + * @return return value documented here + */ + @IntRange(from = 10) + public int test1(int parameter1, int parameter2, int parameter3) { } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class RangeTest { + public RangeTest() { throw new RuntimeException("Stub!"); } + /** + * This is the existing documentation. + * @return return value documented here + * Value is 10 or greater + */ + @android.support.annotation.IntRange(from=10) public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `test documentation trim utility`() { + assertEquals( + "/**\n * This is a comment\n * This is a second comment\n */", + trimDocIndent( + """/** + * This is a comment + * This is a second comment + */ + """.trimIndent() + ) + ) + } + + @Test + fun `Merge API levels)`() { + check( + sourceFiles = *arrayOf( + java( + """ + package android.widget; + + public class Toolbar { + public int getCurrentContentInsetEnd() { + return 0; + } + } + """ + ), + intRangeAnnotationSource + ), + checkCompilation = true, + checkDoclava1 = false, + applyApiLevelsXml = """ + <?xml version="1.0" encoding="utf-8"?> + <api version="2"> + <class name="android/widget/Toolbar" since="21"> + <method name="<init>(Landroid/content/Context;)V"/> + <method name="collapseActionView()V"/> + <method name="getContentInsetStartWithNavigation()I" since="24"/> + <method name="getCurrentContentInsetEnd()I" since="24"/> + <method name="getCurrentContentInsetLeft()I" since="24"/> + <method name="getCurrentContentInsetRight()I" since="24"/> + <method name="getCurrentContentInsetStart()I" since="24"/> + </class> + </api> + """, + stubs = arrayOf( + """ + package android.widget; + /** + * Requires API level 21 + */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Toolbar { + public Toolbar() { throw new RuntimeException("Stub!"); } + /** + * Requires API level 24 + */ + public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Merge deprecation levels)`() { + check( + sourceFiles = *arrayOf( + java( + """ + package android.hardware; + /** + * The Camera class is used to set image capture settings, start/stop preview. + * + * @deprecated We recommend using the new {@link android.hardware.camera2} API for new + * applications.* + */ + @Deprecated + public class Camera { + /** @deprecated Use something else. */ + public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO"; + } + """ + ) + ), + applyApiLevelsXml = """ + <?xml version="1.0" encoding="utf-8"?> + <api version="2"> + <class name="android/hardware/Camera" since="1" deprecated="21"> + <method name="<init>()V"/> + <method name="addCallbackBuffer([B)V" since="8"/> + <method name="getLogo()Landroid/graphics/drawable/Drawable;"/> + <field name="ACTION_NEW_VIDEO" since="14" deprecated="25"/> + </class> + </api> + """, + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package android.hardware; + /** + * The Camera class is used to set image capture settings, start/stop preview. + * + * @deprecated + * <p class="caution"><strong>This class was deprecated in API level 21.</strong></p> + * We recommend using the new {@link android.hardware.camera2} API for new + * applications.* + */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @Deprecated public class Camera { + public Camera() { throw new RuntimeException("Stub!"); } + /** + * + * Requires API level 14 + * @deprecated + * <p class="caution"><strong>This class was deprecated in API level 21.</strong></p> + * Use something else. */ + @Deprecated public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO"; + } + """ + ) + ) + } +} \ 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 new file mode 100644 index 0000000..281aab7 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -0,0 +1,1160 @@ +/* + * Copyright (C) 2017 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.SdkConstants +import com.android.SdkConstants.DOT_JAVA +import com.android.SdkConstants.DOT_KT +import com.android.SdkConstants.VALUE_TRUE +import com.android.annotations.NonNull +import com.android.ide.common.process.DefaultProcessExecutor +import com.android.ide.common.process.LoggedProcessOutputHandler +import com.android.ide.common.process.ProcessException +import com.android.ide.common.process.ProcessInfoBuilder +import com.android.tools.lint.checks.ApiLookup +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.stripComments +import com.android.tools.metalava.doclava1.Errors +import com.android.utils.FileUtils +import com.android.utils.StdLogger +import com.google.common.base.Charsets +import com.google.common.io.Files +import org.intellij.lang.annotations.Language +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter + +const val CHECK_OLD_DOCLAVA_TOO = false +const val CHECK_STUB_COMPILATION = false +const val SKIP_NON_COMPAT = false + +abstract class DriverTest { + @get:Rule + var temporaryFolder = TemporaryFolder() + + protected fun createProject(vararg files: TestFile): File { + val dir = temporaryFolder.newFolder() + + files + .map { it.createFile(dir) } + .forEach { assertNotNull(it) } + + return dir + } + + protected fun runDriver(vararg args: String): String { + val sw = StringWriter() + val writer = PrintWriter(sw) + if (!com.android.tools.metalava.run(arrayOf(*args), writer, writer)) { + fail(sw.toString()) + } + + return sw.toString() + } + + private fun findKotlinStdlibPath(): List<String> { + val classPath: String = System.getProperty("java.class.path") + val paths = mutableListOf<String>() + for (path in classPath.split(':')) { + val file = File(path) + val name = file.name + if (name.startsWith("kotlin-stdlib") || + name.startsWith("kotlin-reflect") || + name.startsWith("kotlin-script-runtime") + ) { + paths.add(file.path) + } + } + if (paths.isEmpty()) { + error("Did not find kotlin-stdlib-jre8 in $PROGRAM_NAME classpath: $classPath") + } + return paths + } + + private fun getJdkPath(): String? { + val javaHome = System.getProperty("java.home") + if (javaHome != null) { + var javaHomeFile = File(javaHome) + if (File(javaHomeFile, "bin${File.separator}javac").exists()) { + return javaHome + } else if (javaHomeFile.name == "jre") { + javaHomeFile = javaHomeFile.parentFile + if (javaHomeFile != null && File(javaHomeFile, "bin${File.separator}javac").exists()) { + return javaHomeFile.path + } + } + } + return System.getenv("JAVA_HOME") + } + + protected fun check( + /** The source files to pass to the analyzer */ + vararg sourceFiles: TestFile, + /** The API signature content (corresponds to --api) */ + api: String? = null, + /** The exact API signature content (corresponds to --exact-api) */ + exactApi: String? = null, + /** The removed API (corresponds to --removed-api) */ + removedApi: String? = null, + /** The private API (corresponds to --private-api) */ + privateApi: String? = null, + /** The private DEX API (corresponds to --private-dex-api) */ + privateDexApi: String? = null, + /** Expected stubs (corresponds to --stubs) */ + @Language("JAVA") stubs: Array<String> = emptyArray(), + /** Stub source file list generated */ + stubsSourceList: String? = null, + /** Whether to run in doclava1 compat mode */ + compatibilityMode: Boolean = true, + /** Whether to trim the output (leading/trailing whitespace removal) */ + trim: Boolean = true, + /** Whether to remove blank lines in the output (the signature file usually contains a lot of these) */ + stripBlankLines: Boolean = true, + /** Warnings expected to be generated when analyzing these sources */ + warnings: String? = "", + /** Whether to run doclava1 on the test output and assert that the output is identical */ + checkDoclava1: Boolean = compatibilityMode, + checkCompilation: Boolean = false, + /** Annotations to merge in */ + @Language("XML") mergeAnnotations: String? = null, + /** An optional API signature file content to load **instead** of Java/Kotlin source files */ + @Language("TEXT") signatureSource: String? = null, + /** An optional API signature representing the previous API level to diff */ + @Language("TEXT") previousApi: String? = null, + /** An optional Proguard keep file to generate */ + @Language("Proguard") proguard: String? = null, + /** Whether we should migrate nullness information */ + migrateNulls: Boolean = false, + /** Whether we should check compatibility */ + checkCompatibility: Boolean = false, + /** Show annotations (--show-annotation arguments) */ + showAnnotations: Array<String> = emptyArray(), + /** If using [showAnnotations], whether to include unannotated */ + showUnannotated: Boolean = false, + /** Additional arguments to supply */ + extraArguments: Array<String> = emptyArray(), + /** Whether we should emit Kotlin-style null signatures */ + outputKotlinStyleNulls: Boolean = !compatibilityMode, + /** Whether we should interpret API files being read as having Kotlin-style nullness types */ + inputKotlinStyleNulls: Boolean = false, + /** Whether we should omit java.lang. etc from signature files */ + omitCommonPackages: Boolean = !compatibilityMode, + /** Expected output (stdout and stderr combined). If null, don't check. */ + expectedOutput: String? = null, + /** List of extra jar files to record annotation coverage from */ + coverageJars: Array<TestFile>? = null, + /** Optional manifest to load and associate with the codebase */ + @Language("XML") + manifest: String? = null, + /** Packages to pre-import (these will therefore NOT be included in emitted stubs, signature files etc */ + importedPackages: List<String> = emptyList(), + /** Packages to skip emitting signatures/stubs for even if public (typically used for unit tests + * referencing to classpath classes that aren't part of the definitions and shouldn't be part of the + * test output; e.g. a test may reference java.lang.Enum but we don't want to start reporting all the + * public APIs in the java.lang package just because it's indirectly referenced via the "enum" superclass + */ + skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"), + /** Whether we should include --showAnnotations=android.annotation.SystemApi */ + includeSystemApiAnnotations: Boolean = false, + /** Whether we should warn about super classes that are stripped because they are hidden */ + includeStrippedSuperclassWarnings: Boolean = false, + /** Apply level to XML */ + applyApiLevelsXml: String? = null + ) { + System.setProperty("METALAVA_TESTS_RUNNING", VALUE_TRUE) + + if (compatibilityMode && mergeAnnotations != null) { + fail( + "Can't specify both compatibilityMode and mergeAnnotations: there were no " + + "annotations output in doclava1" + ) + } + + Errors.resetLevels() + + // Unit test which checks that a signature file is as expected + val androidJar = getPlatformFile("android.jar") + + val project = createProject(*sourceFiles) + + val packages = sourceFiles.asSequence().map { findPackage(it.getContents()!!) }.filterNotNull().toSet() + + val sourcePathDir = File(project, "src") + val sourcePath = sourcePathDir.path + val sourceList = + if (signatureSource != null) { + sourcePathDir.mkdirs() + assert(sourceFiles.isEmpty(), { "Shouldn't combine sources with signature file loads" }) + val signatureFile = File(project, "load-api.txt") + Files.asCharSink(signatureFile, Charsets.UTF_8).write(signatureSource.trimIndent()) + if (includeStrippedSuperclassWarnings) { + arrayOf(signatureFile.path) + } else { + arrayOf( + signatureFile.path, + "--hide", + "HiddenSuperclass" + ) // Suppress warning #111 + } + } else { + sourceFiles.asSequence().map { File(project, it.targetPath).path }.toList().toTypedArray() + } + + val reportedWarnings = StringBuilder() + reporter = object : Reporter(project) { + override fun print(message: String) { + reportedWarnings.append(message.replace(project.path, "TESTROOT").trim()).append('\n') + } + } + + val mergeAnnotationsArgs = if (mergeAnnotations != null) { + val merged = File(project, "merged-annotations.xml") + Files.asCharSink(merged, Charsets.UTF_8).write(mergeAnnotations.trimIndent()) + arrayOf("--merge-annotations", merged.path) + } else { + emptyArray() + } + + val previousApiFile = if (previousApi != null) { + val file = File(project, "previous-api.txt") + Files.asCharSink(file, Charsets.UTF_8).write(previousApi.trimIndent()) + file + } else { + null + } + + val previousApiArgs = if (previousApiFile != null) { + arrayOf("--previous-api", previousApiFile.path) + } else { + emptyArray() + } + + val manifestFileArgs = if (manifest != null) { + val file = File(project, "manifest.xml") + Files.asCharSink(file, Charsets.UTF_8).write(manifest.trimIndent()) + arrayOf("--manifest", file.path) + } else { + emptyArray() + } + + val migrateNullsArguments = if (migrateNulls) { + arrayOf("--migrate-nullness") + } else { + emptyArray() + } + + val checkCompatibilityArguments = if (checkCompatibility) { + arrayOf("--check-compatibility") + } else { + emptyArray() + } + + val quiet = if (expectedOutput != null && !extraArguments.contains("--verbose")) { + // If comparing output, avoid noisy output such as the banner etc + arrayOf("--quiet") + } else { + emptyArray() + } + + val coverageStats = if (coverageJars != null && coverageJars.isNotEmpty()) { + val sb = StringBuilder() + val root = File(project, "coverageJars") + root.mkdirs() + for (jar in coverageJars) { + if (sb.isNotEmpty()) { + sb.append(File.pathSeparator) + } + val file = jar.createFile(root) + sb.append(file.path) + } + arrayOf("--annotation-coverage-of", sb.toString()) + } else { + emptyArray() + } + + var proguardFile: File? = null + val proguardKeepArguments = if (proguard != null) { + proguardFile = File(project, "proguard.cfg") + arrayOf("--proguard", proguardFile.path) + } else { + emptyArray() + } + + val showAnnotationArguments = if (showAnnotations.isNotEmpty() || includeSystemApiAnnotations) { + val args = mutableListOf<String>() + for (annotation in showAnnotations) { + args.add("--show-annotation") + args.add(annotation) + } + if (includeSystemApiAnnotations && !args.contains("android.annotation.SystemApi")) { + args.add("--show-annotation") + args.add("android.annotation.SystemApi") + } + args.toTypedArray() + } else { + emptyArray() + } + + val showUnannotatedArgs = + if (showUnannotated) { + arrayOf("--show-unannotated") + } else { + emptyArray<String>() + } + + var removedApiFile: File? = null + val removedArgs = if (removedApi != null) { + removedApiFile = temporaryFolder.newFile("removed.txt") + arrayOf("--removed-api", removedApiFile.path) + } else { + emptyArray() + } + + var apiFile: File? = null + val apiArgs = if (api != null) { + apiFile = temporaryFolder.newFile("api.txt") + arrayOf("--api", apiFile.path) + } else { + emptyArray() + } + + var exactApiFile: File? = null + val exactApiArgs = if (exactApi != null) { + exactApiFile = temporaryFolder.newFile("exact-api.txt") + arrayOf("--exact-api", exactApiFile.path) + } else { + emptyArray() + } + + var privateApiFile: File? = null + val privateApiArgs = if (privateApi != null) { + privateApiFile = temporaryFolder.newFile("private.txt") + arrayOf("--private-api", privateApiFile.path) + } else { + emptyArray() + } + + var privateDexApiFile: File? = null + val privateDexApiArgs = if (privateDexApi != null) { + privateDexApiFile = temporaryFolder.newFile("private-dex.txt") + arrayOf("--private-dex-api", privateDexApiFile.path) + } else { + emptyArray() + } + + var stubsDir: File? = null + val stubsArgs = if (stubs.isNotEmpty()) { + stubsDir = temporaryFolder.newFolder("stubs") + arrayOf("--stubs", stubsDir.path) + } else { + emptyArray() + } + + var stubsSourceListFile: File? = null + val stubsSourceListArgs = if (stubsSourceList != null) { + stubsSourceListFile = temporaryFolder.newFile("droiddoc-src-list") + arrayOf("--write-stubs-source-list", stubsSourceListFile.path) + } else { + emptyArray() + } + + val applyApiLevelsXmlFile: File? + val applyApiLevelsXmlArgs = if (applyApiLevelsXml != null) { + ApiLookup::class.java.getDeclaredMethod("dispose").apply { isAccessible = true }.invoke(null) + applyApiLevelsXmlFile = temporaryFolder.newFile("api-versions.xml") + Files.asCharSink(applyApiLevelsXmlFile!!, Charsets.UTF_8).write(applyApiLevelsXml.trimIndent()) + arrayOf("--apply-api-levels", applyApiLevelsXmlFile.path) + } else { + emptyArray() + } + + val importedPackageArgs = mutableListOf<String>() + importedPackages.forEach { + importedPackageArgs.add("--stub-import-packages") + importedPackageArgs.add(it) + } + + val skipEmitPackagesArgs = mutableListOf<String>() + skipEmitPackages.forEach { + skipEmitPackagesArgs.add("--skip-emit-packages") + skipEmitPackagesArgs.add(it) + } + + val kotlinPath = findKotlinStdlibPath() + val kotlinPathArgs = + if (kotlinPath.isNotEmpty() && + sourceList.asSequence().any { it.endsWith(DOT_KT) } + ) { + arrayOf("--classpath", kotlinPath.joinToString(separator = File.pathSeparator) { it }) + } else { + emptyArray() + } + + val actualOutput = runDriver( + "--no-color", + + // For the tests we want to treat references to APIs like java.io.Closeable + // as a class that is part of the API surface, not as a hidden class as would + // be the case when analyzing a complete API surface + //"--unhide-classpath-classes", + "--allow-referencing-unknown-classes", + + "--sourcepath", + sourcePath, + "--classpath", + androidJar.path, + *kotlinPathArgs, + *removedArgs, + *apiArgs, + *exactApiArgs, + *privateApiArgs, + *privateDexApiArgs, + *stubsArgs, + *stubsSourceListArgs, + "--compatible-output=${if (compatibilityMode) "yes" else "no"}", + "--output-kotlin-nulls=${if (outputKotlinStyleNulls) "yes" else "no"}", + "--input-kotlin-nulls=${if (inputKotlinStyleNulls) "yes" else "no"}", + "--omit-common-packages=${if (omitCommonPackages) "yes" else "no"}", + *coverageStats, + *quiet, + *mergeAnnotationsArgs, + *previousApiArgs, + *migrateNullsArguments, + *checkCompatibilityArguments, + *proguardKeepArguments, + *manifestFileArgs, + *applyApiLevelsXmlArgs, + *showAnnotationArguments, + *showUnannotatedArgs, + *importedPackageArgs.toTypedArray(), + *skipEmitPackagesArgs.toTypedArray(), + *extraArguments, + *sourceList + ) + + if (expectedOutput != null) { + assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim()) + } + + if (api != null && apiFile != null) { + assertTrue("${apiFile.path} does not exist even though --api was used", apiFile.exists()) + val expectedText = readFile(apiFile, stripBlankLines, trim) + assertEquals(stripComments(api, stripLineComments = false).trimIndent(), expectedText) + } + + if (removedApi != null && removedApiFile != null) { + assertTrue( + "${removedApiFile.path} does not exist even though --removed-api was used", + removedApiFile.exists() + ) + val expectedText = readFile(removedApiFile, stripBlankLines, trim) + assertEquals(stripComments(removedApi, stripLineComments = false).trimIndent(), expectedText) + } + + if (exactApi != null && exactApiFile != null) { + assertTrue( + "${exactApiFile.path} does not exist even though --exact-api was used", + exactApiFile.exists() + ) + val expectedText = readFile(exactApiFile, stripBlankLines, trim) + assertEquals(stripComments(exactApi, stripLineComments = false).trimIndent(), expectedText) + } + + if (privateApi != null && privateApiFile != null) { + assertTrue( + "${privateApiFile.path} does not exist even though --private-api was used", + privateApiFile.exists() + ) + val expectedText = readFile(privateApiFile, stripBlankLines, trim) + assertEquals(stripComments(privateApi, stripLineComments = false).trimIndent(), expectedText) + } + + if (privateDexApi != null && privateDexApiFile != null) { + assertTrue( + "${privateDexApiFile.path} does not exist even though --private-dex-api was used", + privateDexApiFile.exists() + ) + val expectedText = readFile(privateDexApiFile, stripBlankLines, trim) + assertEquals(stripComments(privateDexApi, stripLineComments = false).trimIndent(), expectedText) + } + + if (proguard != null && proguardFile != null) { + val expectedProguard = readFile(proguardFile) + assertTrue( + "${proguardFile.path} does not exist even though --proguard was used", + proguardFile.exists() + ) + assertEquals(stripComments(proguard, stripLineComments = false).trimIndent(), expectedProguard.trim()) + } + + if (warnings != null) { + assertEquals( + warnings.trimIndent().trim(), + reportedWarnings.toString().replace(project.path, "TESTROOT").replace( + project.canonicalPath, + "TESTROOT" + ).split("\n").sorted().joinToString(separator = "\n").trim() + ) + } + + if (stubs.isNotEmpty() && stubsDir != null) { + for (i in 0 until stubs.size) { + val stub = stubs[i] + val sourceFile = sourceFiles[i] + val targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) { + // Kotlin source stubs are rewritten as .java files for now + sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA + } else { + sourceFile.targetPath + } + val stubFile = File(stubsDir, targetPath.substring("src/".length)) + val expectedText = readFile(stubFile, stripBlankLines, trim) + assertEquals(stub.trimIndent(), expectedText) + } + } + + if (stubsSourceList != null && stubsSourceListFile != null) { + assertTrue( + "${stubsSourceListFile.path} does not exist even though --write-stubs-source-list was used", + stubsSourceListFile.exists() + ) + val expectedText = readFile(stubsSourceListFile, stripBlankLines, trim) + assertEquals(stripComments(stubsSourceList, stripLineComments = false).trimIndent(), expectedText) + } + + if (checkCompilation && stubsDir != null && CHECK_STUB_COMPILATION) { + val generated = gatherSources(listOf(stubsDir)).map { it.path }.toList().toTypedArray() + + // Also need to include on the compile path annotation classes referenced in the stubs + val supportAnnotationsDir = File("../../frameworks/support/annotations/src/main/java/") + if (!supportAnnotationsDir.isDirectory) { + fail("Couldn't find $supportAnnotationsDir: Is the pwd set to the root of the metalava source code?") + } + val supportAnnotations = + gatherSources(listOf(supportAnnotationsDir)).map { it.path }.toList().toTypedArray() + + val extraAnnotationsDir = File("stub-annotations/src/main/java") + if (!extraAnnotationsDir.isDirectory) { + fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of the metalava source code?") + fail("Couldn't find $extraAnnotationsDir: Is the pwd set to the root of an Android source tree?") + } + val extraAnnotations = gatherSources(listOf(extraAnnotationsDir)).map { it.path }.toList().toTypedArray() + + + if (!runCommand( + "${getJdkPath()}/bin/javac", arrayOf( + "-d", project.path, *generated, + *supportAnnotations, *extraAnnotations + ) + ) + ) { + fail("Couldn't compile stub file -- compilation problems") + return + } + } + + if (checkDoclava1 && !CHECK_OLD_DOCLAVA_TOO) { + println( + "This test requested diffing with doclava1, but doclava1 testing was disabled with the " + + "DriverTest#CHECK_OLD_DOCLAVA_TOO = false" + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && + api != null && apiFile != null + ) { + apiFile.delete() + checkSignaturesWithDoclava1( + api = api, + argument = "-api", + output = apiFile, + expected = apiFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && + exactApi != null && exactApiFile != null + ) { + exactApiFile.delete() + checkSignaturesWithDoclava1( + api = exactApi, + argument = "-exactApi", + output = exactApiFile, + expected = exactApiFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null + && removedApi != null && removedApiFile != null + ) { + removedApiFile.delete() + checkSignaturesWithDoclava1( + api = removedApi, + argument = "-removedApi", + output = removedApiFile, + expected = removedApiFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && stubsDir != null) { + stubsDir.deleteRecursively() + val firstFile = File(stubsDir, sourceFiles[0].targetPath.substring("src/".length)) + checkSignaturesWithDoclava1( + api = stubs[0], + argument = "-stubs", + output = stubsDir, + expected = firstFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && proguard != null && proguardFile != null) { + proguardFile.delete() + checkSignaturesWithDoclava1( + api = proguard, + argument = "-proguard", + output = proguardFile, + expected = proguardFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && + privateApi != null && privateApiFile != null + ) { + privateApiFile.delete() + checkSignaturesWithDoclava1( + api = privateApi, + argument = "-privateApi", + output = privateApiFile, + expected = privateApiFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + // Workaround: -privateApi is a no-op if you don't also provide -api + extraArguments = arrayOf("-api", File(privateApiFile.parentFile, "dummy-api.txt").path), + showUnannotated = showUnannotated + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && + privateDexApi != null && privateDexApiFile != null + ) { + privateDexApiFile.delete() + checkSignaturesWithDoclava1( + api = privateDexApi, + argument = "-privateDexApi", + output = privateDexApiFile, + expected = privateDexApiFile, + sourceList = sourceList, + sourcePath = sourcePath, + packages = packages, + androidJar = androidJar, + trim = trim, + stripBlankLines = stripBlankLines, + showAnnotationArgs = showAnnotationArguments, + stubImportPackages = importedPackages, + // Workaround: -privateDexApi is a no-op if you don't also provide -api + extraArguments = arrayOf("-api", File(privateDexApiFile.parentFile, "dummy-api.txt").path), + showUnannotated = showUnannotated + ) + } + } + + private fun checkSignaturesWithDoclava1( + api: String, + argument: String, + output: File, + expected: File = output, + sourceList: Array<String>, + sourcePath: String, + packages: Set<String>, + androidJar: File, + trim: Boolean = true, + stripBlankLines: Boolean = true, + showAnnotationArgs: Array<String> = emptyArray(), + stubImportPackages: List<String>, + extraArguments: Array<String> = emptyArray(), + showUnannotated: Boolean + ) { + // We have to run Doclava out of process because running it in process + // (with Doclava1 jars on the test classpath) only works once; it leaves + // around state such that the second test fails. Instead we invoke it + // separately on each test; slower but reliable. + + val doclavaArg = when (argument) { + "--api" -> "-api" + "--removed-api" -> "-removedApi" + else -> if (argument.startsWith("--")) argument.substring(1) else argument + } + + val showAnnotationArgsDoclava1: Array<String> = if (showAnnotationArgs.isNotEmpty()) { + showAnnotationArgs.map { if (it == "--show-annotation") "-showAnnotation" else it }.toTypedArray() + } else { + emptyArray() + } + val showUnannotatedArgs = if (showUnannotated) { + arrayOf("-showUnannotated") + } else { + emptyArray() + } + + val docLava1 = File("testlibs/doclava-1.0.6-full-SNAPSHOT.jar") + if (!docLava1.isFile) { + /* + Not checked in (it's 22MB). + To generate the doclava1 jar, add this to external/doclava/build.gradle and run ./gradlew shadowJar: + + // shadow jar: Includes all dependencies + buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2' + } + } + apply plugin: 'com.github.johnrengelman.shadow' + shadowJar { + baseName = "doclava-$version-full-SNAPSHOT" + classifier = null + version = null + } + */ + fail("Couldn't find $docLava1: Is the pwd set to the root of the metalava source code?") + } + + val jdkPath = getJdkPath() + if (jdkPath == null) { + fail("JDK not found in the environment; make sure \$JAVA_HOME is set.") + } + + val hidePackageArgs = mutableListOf<String>() + options.hidePackages.forEach { + hidePackageArgs.add("-hidePackage") + hidePackageArgs.add(it) + } + + val stubImports = if (stubImportPackages.isNotEmpty()) { + arrayOf("-stubimportpackages", stubImportPackages.joinToString(separator = ":") { it }) + } else { + emptyArray() + } + + val args = arrayOf<String>( + *sourceList, + "-stubpackages", + packages.joinToString(separator = ":") { it }, + *stubImports, + "-doclet", + "com.google.doclava.Doclava", + "-docletpath", + docLava1.path, + "-encoding", + "UTF-8", + "-source", + "1.8", + "-nodocs", + "-quiet", + "-sourcepath", + sourcePath, + "-classpath", + androidJar.path, + + *showAnnotationArgsDoclava1, + *showUnannotatedArgs, + *hidePackageArgs.toTypedArray(), + *extraArguments, + + // -api, or // -stub, etc + doclavaArg, + output.path + ) + + + val message = "\n${args.joinToString(separator = "\n") { "\"$it\"," }}" + println("Running doclava1 with the following args:\n$message") + + + if (!runCommand( + "$jdkPath/bin/java", + arrayOf( + "-classpath", + "${docLava1.path}:$jdkPath/lib/tools.jar", + "com.google.doclava.Doclava", + *args + ) + ) + ) { + return + } + + val expectedText = readFile(expected, stripBlankLines, trim) + assertEquals(stripComments(api, stripLineComments = false).trimIndent(), expectedText) + } + + private fun runCommand(executable: String, args: Array<String>): Boolean { + try { + val logger = StdLogger(StdLogger.Level.ERROR) + val processExecutor = DefaultProcessExecutor(logger) + val processInfo = ProcessInfoBuilder() + .setExecutable(executable) + .addArgs(args) + .createProcess() + + val processOutputHandler = LoggedProcessOutputHandler(logger) + val result = processExecutor.execute(processInfo, processOutputHandler) + + result.rethrowFailure().assertNormalExitValue() + } catch (e: ProcessException) { + fail("Failed to run $executable (${e.message}): not verifying this API on the old doclava engine") + return false + } + return true + } + + companion object { + private val apiLevel = "27" + + private val latestAndroidPlatform: String + get() = "android-$apiLevel" + + private val sdk: File + get() = File( + System.getenv("ANDROID_HOME") + ?: error("You must set \$ANDROID_HOME before running tests") + ) + + fun getPlatformFile(path: String): File { + val localFile = File("../../prebuilts/sdk/$apiLevel/android.jar") + if (localFile.exists()) { + return localFile + } + val file = FileUtils.join(sdk, SdkConstants.FD_PLATFORMS, latestAndroidPlatform, path) + if (!file.exists()) { + throw IllegalArgumentException( + "File \"$path\" not found in platform ${latestAndroidPlatform}" + ) + } + return file + } + + @NonNull + fun java(@NonNull @Language("JAVA") source: String): LintDetectorTest.TestFile { + return TestFiles.java(source.trimIndent()) + } + + @NonNull + fun kotlin(@NonNull @Language("kotlin") source: String): LintDetectorTest.TestFile { + return TestFiles.kotlin(source.trimIndent()) + } + + private fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String { + var apiLines: List<String> = Files.asCharSource(file, Charsets.UTF_8).readLines() + if (stripBlankLines) { + apiLines = apiLines.asSequence().filter { it.isNotBlank() }.toList() + } + var apiText = apiLines.joinToString(separator = "\n") { it } + if (trim) { + apiText = apiText.trim() + } + return apiText + } + } +} + +val intRangeAnnotationSource: 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({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE}) + public @interface IntRange { + 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; +} +""" +) + +val nonNullSource: TestFile = java( + """ +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 { +} + +""" +) + +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; +} + """ +) + +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 { +} + """ +) + +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 { +} + +""" +) + +val supportNullableSource: 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 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(); +} + +""" +) + +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 { +} + """ +) + +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 { +} + """ +) + +val suppressLintSource: TestFile = java( + """ +package android.annotation; + +import static java.lang.annotation.ElementType.*; +import java.lang.annotation.*; +@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) +@Retention(RetentionPolicy.CLASS) +public @interface SuppressLint { + String[] value(); +} + """ +) + +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(); +} + """ +) + +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 { +} +""" +) + diff --git a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt new file mode 100644 index 0000000..517abd7 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2017 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.utils.SdkUtils.fileToUrlString +import com.google.common.base.Charsets +import com.google.common.io.ByteStreams +import com.google.common.io.Closeables +import com.google.common.io.Files +import org.intellij.lang.annotations.Language +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.net.URL + +@SuppressWarnings("ALL") // Sample code +class ExtractAnnotationsTest : DriverTest() { + + @Test + fun `Include class retention`() { + val androidJar = getPlatformFile("android.jar") + + val project = createProject( + packageTest, + genericTest, + intDefTest, + permissionsTest, + manifest, + intDefAnnotation, + intRangeAnnotation, + permissionAnnotation, + nullableAnnotation + ) + + val output = temporaryFolder.newFile("annotations.zip") + + runDriver( + "--sources", + File(project, "src").path, + "--classpath", + androidJar.path, + "--extract-annotations", + output.path + ) + + // Check extracted annotations + checkPackageXml( + "test.pkg", output, """<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="test.pkg"> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="20" /> + </annotation> + </item> + <item name="test.pkg.IntDefTest void setFlags(java.lang.Object, int) 1"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 4}" /> + <val name="flag" val="true" /> + </annotation> + </item> + <item name="test.pkg.IntDefTest void setStyle(int, int) 0"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}" /> + </annotation> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="20" /> + </annotation> + </item> + <item name="test.pkg.IntDefTest.Inner void setInner(int) 0"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 4}" /> + <val name="flag" val="true" /> + </annotation> + </item> + <item name="test.pkg.MyEnhancedList"> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="0" /> + </annotation> + </item> + <item name="test.pkg.MyEnhancedList E getReversed(java.util.List<java.lang.String>, java.util.Comparator<? super E>)"> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="10" /> + </annotation> + </item> + <item name="test.pkg.MyEnhancedList java.lang.String getPrefix()"> + <annotation name="android.support.annotation.Nullable" /> + </item> + <item name="test.pkg.PermissionsTest CONTENT_URI"> + <annotation name="android.support.annotation.RequiresPermission.Read"> + <val name="value" val=""android.permission.MY_READ_PERMISSION_STRING"" /> + </annotation> + <annotation name="android.support.annotation.RequiresPermission.Write"> + <val name="value" val=""android.permission.MY_WRITE_PERMISSION_STRING"" /> + </annotation> + </item> + <item name="test.pkg.PermissionsTest void myMethod()"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="value" val=""android.permission.MY_PERMISSION_STRING"" /> + </annotation> + </item> + <item name="test.pkg.PermissionsTest void myMethod2()"> + <annotation name="android.support.annotation.RequiresPermission"> + <val name="anyOf" val="{"android.permission.MY_PERMISSION_STRING", "android.permission.MY_PERMISSION_STRING2"}" /> + </annotation> + </item> +</root> + +""" + ) + } + + @Test + fun `Skip class retention`() { + val androidJar = getPlatformFile("android.jar") + + val project = createProject( + intDefTest, + permissionsTest, + manifest, + intDefAnnotation, + intRangeAnnotation, + permissionAnnotation + ) + + val output = temporaryFolder.newFile("annotations.zip") + + runDriver( + "--sources", + File(project, "src").path, + "--classpath", + androidJar.path, + "--skip-class-retention", + "--extract-annotations", + output.path + ) + + // Check external annotations + checkPackageXml( + "test.pkg", output, + """<?xml version="1.0" encoding="UTF-8"?> +<root> + <item name="test.pkg.IntDefTest void setFlags(java.lang.Object, int) 1"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 4}" /> + <val name="flag" val="true" /> + </annotation> + </item> + <item name="test.pkg.IntDefTest void setStyle(int, int) 0"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}" /> + </annotation> + <annotation name="android.support.annotation.IntRange"> + <val name="from" val="20" /> + </annotation> + </item> + <item name="test.pkg.IntDefTest.Inner void setInner(int) 0"> + <annotation name="android.support.annotation.IntDef"> + <val name="value" val="{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 4}" /> + <val name="flag" val="true" /> + </annotation> + </item> +</root> + +""" + ) + } + + @Test + fun `Test writing jar recipe file`() { + val androidJar = getPlatformFile("android.jar") + + val project = createProject( + intDefTest, + permissionsTest, + manifest, + intDefAnnotation, + intRangeAnnotation, + permissionAnnotation + ) + + val output = temporaryFolder.newFile("annotations.zip") + val typedefFile = temporaryFolder.newFile("typedefs.txt") + + runDriver( + "--sources", + File(project, "src").path, + "--classpath", + androidJar.path, + + "--extract-annotations", + output.path, + "--typedef-file", + typedefFile.path + ) + + // Check recipe + assertEquals( + """D test/pkg/IntDefTest${"$"}DialogFlags +D test/pkg/IntDefTest${"$"}DialogStyle +""", + Files.asCharSource(typedefFile, Charsets.UTF_8).read() + ) + } + + @SuppressWarnings("all") // sample code + private val intDefAnnotation = java( + """ + package android.support.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() + + @SuppressWarnings("all") // sample code + private val intRangeAnnotation = java( + """ + package android.support.annotation; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.CLASS; + + @Retention(CLASS) + @Target({CONSTRUCTOR,METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE}) + public @interface IntRange { + long from() default Long.MIN_VALUE; + long to() default Long.MAX_VALUE; + } + """ + ).indented() + + @SuppressWarnings("all") // sample code + private val permissionAnnotation = java( + """ + package android.support.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.*; + @Retention(CLASS) + @Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD}) + public @interface RequiresPermission { + String value() default ""; + String[] allOf() default {}; + String[] anyOf() default {}; + boolean conditional() default false; + @Target(FIELD) + @interface Read { + RequiresPermission value(); + } + @Target(FIELD) + @interface Write { + RequiresPermission value(); + } + }""" + ).indented() + + @SuppressWarnings("all") // sample code + private val nullableAnnotation = java( + """ + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.*; + @Retention(CLASS) + @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) + public @interface Nullable { + }""" + ).indented() + + @SuppressWarnings("all") // sample code + private val packageTest = java( + """ + @IntRange(from = 20) + package test.pkg; + + import android.support.annotation.IntRange;""" + ).indented() + + @SuppressWarnings("all") // sample code + private val genericTest = java( + """ + package test.pkg; + + import android.support.annotation.IntRange; + import android.support.annotation.Nullable; + + import java.util.Comparator; + import java.util.List; + + @IntRange(from = 0) + public interface MyEnhancedList<E> extends List<E> { + @IntRange(from = 10) + E getReversed(List<String> filter, Comparator<? super E> comparator); + @Nullable String getPrefix(); + } + """ + ) + + @SuppressWarnings("all") // sample code + private val intDefTest = java( + """ + package test.pkg; + + import android.support.annotation.IntDef; + import android.support.annotation.IntRange; + import android.support.annotation.Keep; + + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + + @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) + public class IntDefTest { + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @IntRange(from = 20) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + public static final int STYLE_NORMAL = 0; + public static final int STYLE_NO_TITLE = 1; + public static final int STYLE_NO_FRAME = 2; + public static final int STYLE_NO_INPUT = 3; + public static final int UNRELATED = 3; + + public void setStyle(@DialogStyle int style, int theme) { + } + + @Keep public void testIntDef(int arg) { + } + @IntDef(value = {STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT, 3, 3 + 1}, flag=true) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogFlags {} + + public void setFlags(Object first, @DialogFlags int flags) { + } + + public static final String TYPE_1 = "type1"; + public static final String TYPE_2 = "type2"; + public static final String UNRELATED_TYPE = "other"; + + public static class Inner { + public void setInner(@DialogFlags int flags) { + } + } + }""" + ).indented() + + @SuppressWarnings("all") // sample code + private val permissionsTest = java( + """ + package test.pkg; + + import android.support.annotation.RequiresPermission; + + public class PermissionsTest { + @RequiresPermission(Manifest.permission.MY_PERMISSION) + public void myMethod() { + } + @RequiresPermission(anyOf={Manifest.permission.MY_PERMISSION,Manifest.permission.MY_PERMISSION2}) + public void myMethod2() { + } + + + @RequiresPermission.Read(@RequiresPermission(Manifest.permission.MY_READ_PERMISSION)) + @RequiresPermission.Write(@RequiresPermission(Manifest.permission.MY_WRITE_PERMISSION)) + public static final String CONTENT_URI = ""; + } + """ + ) + + @SuppressWarnings("all") // sample code + private val manifest = java( + """ + package test.pkg; + + public class Manifest { + public static final class permission { + public static final String MY_PERMISSION = "android.permission.MY_PERMISSION_STRING"; + public static final String MY_PERMISSION2 = "android.permission.MY_PERMISSION_STRING2"; + public static final String MY_READ_PERMISSION = "android.permission.MY_READ_PERMISSION_STRING"; + public static final String MY_WRITE_PERMISSION = "android.permission.MY_WRITE_PERMISSION_STRING"; + } + } + """ + ).indented() + + private fun checkPackageXml(pkg: String, output: File, @Language("XML") expected: String) { + assertNotNull(output) + assertTrue(output.exists()) + val url = URL( + "jar:" + fileToUrlString(output) + "!/" + pkg.replace('.', '/') + + "/annotations.xml" + ) + val stream = url.openStream() + try { + val bytes = ByteStreams.toByteArray(stream) + assertNotNull(bytes) + val xml = String(bytes, Charsets.UTF_8).replace("\r\n", "\n") + assertEquals(expected, xml) + } finally { + Closeables.closeQuietly(stream) + } + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/KeepFileTest.kt b/src/test/java/com/android/tools/metalava/KeepFileTest.kt new file mode 100644 index 0000000..00e17cd --- /dev/null +++ b/src/test/java/com/android/tools/metalava/KeepFileTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 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 org.junit.Test + +class KeepFileTest : DriverTest() { + @Test + fun `Generate Keep file`() { + check( + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface<T extends Object> + extends MyBaseInterface { + } + """ + ), java( + """ + package a.b.c; + @SuppressWarnings("ALL") + public interface MyStream<T, S extends MyStream<T, S>> extends java.lang.AutoCloseable { + } + """ + ), java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface2<T extends Number> + extends MyBaseInterface { + class TtsSpan<C extends MyInterface<?>> { } + abstract class Range<T extends Comparable<? super T>> { + protected String myString; + } + } + """ + ), + java( + """ + package test.pkg; + public interface MyBaseInterface { + void fun(int a, String b); + } + """ + ) + ), + proguard = """ + -keep class a.b.c.MyStream { + } + -keep class test.pkg.MyBaseInterface { + public abstract void fun(int, java.lang.String); + } + -keep class test.pkg.MyInterface { + } + -keep class test.pkg.MyInterface2 { + } + -keep class test.pkg.MyInterface2${"$"}Range { + <init>(); + protected java.lang.String myString; + } + -keep class test.pkg.MyInterface2${"$"}TtsSpan { + <init>(); + } + """ + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt new file mode 100644 index 0000000..fcf9ecc --- /dev/null +++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2017 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 org.junit.Test + +class NullnessMigrationTest : DriverTest() { + @Test + fun `Test Kotlin-style null signatures`() { + check( + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method @Nullable public Double convert1(@NonNull Float); + method @Nullable public Double convert2(@NonNull Float); + method @Nullable public Double convert3(@NonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """, + api = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double! convert0(Float!); + method public Double? convert1(Float); + method public Double? convert2(Float); + method public Double? convert3(Float); + method public Double? convert4(Float); + } + } + """ + ) + } + + @Test + fun `Method which is now marked null should be marked as newly migrated null`() { + check( + migrateNulls = true, + outputKotlinStyleNulls = false, + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public abstract class MyTest { + method @Nullable public Double convert1(Float); + } + } + """, + previousApi = """ + package test.pkg { + public abstract class MyTest { + method public Double convert1(Float); + } + } + """, + api = """ + package test.pkg { + public abstract class MyTest { + method @NewlyNullable public Double convert1(Float); + } + } + """ + ) + } + + @Test + fun `Parameter which is now marked null should be marked as newly migrated null`() { + check( + migrateNulls = true, + outputKotlinStyleNulls = false, + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public abstract class MyTest { + method public Double convert1(@NonNull Float); + } + } + """, + previousApi = """ + package test.pkg { + public abstract class MyTest { + method public Double convert1(Float); + } + } + """, + api = """ + package test.pkg { + public abstract class MyTest { + method public Double convert1(@NewlyNonNull Float); + } + } + """ + ) + } + + @Test + fun `Comprehensive check of migration`() { + check( + migrateNulls = true, + outputKotlinStyleNulls = false, + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method @Nullable public Double convert1(@NonNull Float); + method @Nullable public Double convert2(@NonNull Float); + method @Nullable public Double convert3(@NonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """, + previousApi = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method public Double convert1(Float); + method @NewlyNullable public Double convert2(@NewlyNonNull Float); + method @RecentlyNullable public Double convert3(@RecentlyNonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """, + api = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method @NewlyNullable public Double convert1(@NewlyNonNull Float); + method @RecentlyNullable public Double convert2(@RecentlyNonNull Float); + method @Nullable public Double convert3(@NonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """ + ) + } + + @Test + fun `Comprehensive check of migration, Kotlin-style output`() { + check( + migrateNulls = true, + outputKotlinStyleNulls = true, + compatibilityMode = false, + signatureSource = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method @Nullable public Double convert1(@NonNull Float); + method @Nullable public Double convert2(@NonNull Float); + method @Nullable public Double convert3(@NonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """, + previousApi = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double convert0(Float); + method public Double convert1(Float); + method @NewlyNullable public Double convert2(@NewlyNonNull Float); + method @RecentlyNullable public Double convert3(@RecentlyNonNull Float); + method @Nullable public Double convert4(@NonNull Float); + } + } + """, + api = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public Double! convert0(Float!); + method public Double? convert1(Float); + method public Double? convert2(Float); + method public Double? convert3(Float); + method public Double? convert4(Float); + } + } + """ + ) + } + + @Test + fun `Convert libcore nullness annotations to support`() { + check( + outputKotlinStyleNulls = false, + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Test { + public @libcore.util.NonNull Object compute() { + return 5; + } + } + """ + ), + java( + """ + package libcore.util; + import static java.lang.annotation.ElementType.TYPE_USE; + import static java.lang.annotation.ElementType.TYPE_PARAMETER; + import static java.lang.annotation.RetentionPolicy.SOURCE; + import java.lang.annotation.Documented; + import java.lang.annotation.Retention; + @Documented + @Retention(SOURCE) + @Target({TYPE_USE}) + public @interface NonNull { + int from() default Integer.MIN_VALUE; + int to() default Integer.MAX_VALUE; + } + """ + ) + ), + api = """ + package libcore.util { + public @interface NonNull { + method public abstract int from(); + method public abstract int to(); + } + } + package test.pkg { + public class Test { + ctor public Test(); + method @NonNull public Object compute(); + } + } + """ + ) + } + + @Test + fun `Check type use annotations`() { + check( + outputKotlinStyleNulls = false, + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.Nullable; + import java.util.List; + public class Test { + public @Nullable Integer compute1(@Nullable java.util.List<@Nullable String> list) { + return 5; + } + public @Nullable Integer compute2(@Nullable java.util.List<@Nullable List<?>> list) { + return 5; + } + // TODO arrays + } + """ + ), + supportNonNullSource, + supportNullableSource + ), + extraArguments = arrayOf("--hide-package", "android.support.annotation"), + api = """ + package test.pkg { + public class Test { + ctor public Test(); + method @Nullable public Integer compute1(@Nullable java.util.List<@Nullable String>); + method @Nullable public Integer compute2(@Nullable java.util.List<@Nullable java.util.List<?>>); + } + } + """ + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt new file mode 100644 index 0000000..e9f35ca --- /dev/null +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2017 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 org.junit.Assert.assertEquals +import org.junit.Test +import java.io.PrintWriter +import java.io.StringWriter + +@Suppress("PrivatePropertyName") +class OptionsTest : DriverTest() { + private val DESCRIPTION = """ +$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the signature +files, the SDK stub files, external annotations etc. +""".trimIndent() + + private val FLAGS = """ +Usage: $PROGRAM_NAME <flags> + +General: +--help This message. +--quiet Only include vital output +--verbose Include extra diagnostic output +--color Attempt to colorize the output (defaults to true if + ${"$"}TERM is xterm) +--no-color Do not attempt to colorize the output + +API sources: +--source-files <files> A comma separated list of source files to be + parsed. Can also be @ followed by a path to a text + file containing paths to the full set of files to + parse. +--source-path <paths> One or more directories (separated by `:`) + containing source files (within a package + hierarchy) +--classpath <paths> One or more directories or jars (separated by `:`) + containing classes that should be on the classpath + when parsing the source files +--merge-annotations <file> An external annotations file (using IntelliJ's + external annotations database format) to merge and + overlay the sources +--input-api-jar <file> A .jar file to read APIs from directly +--manifest <file> A manifest file, used to for check permissions to + cross check APIs +--hide-package <package> Remove the given packages from the API even if they + have not been marked with @hide +--show-annotation <annotation class> Include the given annotation in the API analysis +--show-unannotated Include un-annotated public APIs in the signature + file as well + +Extracting Signature Files: +--api <file> Generate a signature descriptor file +--private-api <file> Generate a signature descriptor file listing the + exact private APIs +--private-dex-api <file> Generate a DEX signature descriptor file listing + the exact private APIs +--removed-api <file> Generate a signature descriptor file for APIs that + have been removed +--output-kotlin-nulls[=yes|no] Controls whether nullness annotations should be + formatted as in Kotlin (with "?" for nullable + types, "" for non nullable types, and "!" for + unknown. The default is yes. +--compatible-output=[yes|no] Controls whether to keep signature files compatible + with the historical format (with its various + quirks) or to generate the new format (which will + also include annotations that are part of the API, + etc.) +--omit-common-packages[=yes|no] Skip common package prefixes like java.lang.* and + kotlin.* in signature files, along with packages + for well known annotations like @Nullable and + @NonNull. +--proguard <file> Write a ProGuard keep file for the API + +Generating Stubs: +--stubs <dir> Generate stub source files for the API +--exclude-annotations Exclude annotations such as @Nullable from the stub + files +--write-stubs-source-list <file> Write the list of generated stub files into the + given source list file + +Diffs and Checks: +--previous-api <signature file> A signature file for the previous version of this + API to apply diffs with +--input-kotlin-nulls[=yes|no] Whether the signature file being read should be + interpreted as having encoded its types using + Kotlin style types: a suffix of "?" for nullable + types, no suffix for non nullable types, and "!" + for unknown. The default is no. +--check-compatibility Check compatibility with the previous API +--migrate-nullness Compare nullness information with the previous API + and mark newly annotated APIs as under migration. +--warnings-as-errors Promote all warnings to errors +--lints-as-errors Promote all API lint warnings to errors +--error <id> Report issues of the given id as errors +--warning <id> Report issues of the given id as warnings +--lint <id> Report issues of the given id as having + lint-severity +--hide <id> Hide/skip issues of the given id + +Statistics: +--annotation-coverage-stats Whether metalava should emit coverage statistics + for annotations, listing the percentage of the API + that has been annotated with nullness information. +--annotation-coverage-of <paths> One or more jars (separated by `:`) containing + existing apps that we want to measure annotation + coverage statistics for. The set of API usages in + those apps are counted up and the most frequently + used APIs that are missing annotation metadata are + listed in descending order. +--skip-java-in-coverage-report In the coverage annotation report, skip java.** and + kotlin.** to narrow the focus down to the Android + framework APIs. + +Extracting Annotations: +--extract-annotations <zipfile> Extracts annotations from the source files and + writes them into the given zip file +--api-filter <file> Applies the given signature file as a filter (which + means no classes,methods or fields not found in the + filter will be included.) +--hide-filtered Omit listing APIs that were skipped because of the + --api-filter +--skip-class-retention Do not extract annotations that have class file + retention +--rmtypedefs Delete all the typedef .class files +--typedef-file <file> Writes an typedef annotation class names into the + given file + +Injecting API Levels: +--apply-api-levels <api-versions.xml> Reads an XML file containing API level descriptions + and merges the information into the documentation + +Extracting API Levels: +--generate-api-levels <xmlfile> Reads android.jar SDK files and generates an XML + file recording the API level for each class, method + and field +--android-jar-pattern <pattern> Patterns to use to locate Android JAR files. The + default is + ${"$"}ANDROID_HOME/platforms/android-%/android.jar. +--current-version Sets the current API level of the current source + code +--current-codename Sets the code name for the current source code +--current-jar Points to the current API jar, if any + +""".trimIndent() + + @Test + fun `Test invalid arguments`() { + val args = listOf("--no-color", "--blah-blah-blah") + + val stdout = StringWriter() + val stderr = StringWriter() + com.android.tools.metalava.run( + args = args.toTypedArray(), + stdout = PrintWriter(stdout), + stderr = PrintWriter(stderr) + ) + assertEquals(BANNER + "\n\n", stdout.toString()) + assertEquals( + """ + +Invalid argument --blah-blah-blah + +$FLAGS + +""".trimIndent(), stderr.toString() + ) + } + + @Test + fun `Test help`() { + val args = listOf("--no-color", "--help") + + val stdout = StringWriter() + val stderr = StringWriter() + com.android.tools.metalava.run( + args = args.toTypedArray(), + stdout = PrintWriter(stdout), + stderr = PrintWriter(stderr) + ) + assertEquals("", stderr.toString()) + assertEquals( + """ +$BANNER + + +$DESCRIPTION + +$FLAGS + +""".trimIndent(), stdout.toString() + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/PackageFilterTest.kt b/src/test/java/com/android/tools/metalava/PackageFilterTest.kt new file mode 100644 index 0000000..e092d78 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/PackageFilterTest.kt @@ -0,0 +1,32 @@ +/* + * 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.google.common.truth.Truth.assertThat +import org.junit.Test + +class PackageFilterTest { + @Test + fun test() { + val filter = PackageFilter(mutableListOf("foo.bar", "bar.baz")) + assertThat(filter.matches("foo.bar")).isTrue() + assertThat(filter.matches("bar.baz")).isTrue() + assertThat(filter.matches("foo.bar.baz")).isTrue() + assertThat(filter.matches("foo.barf")).isFalse() + assertThat(filter.matches("foo")).isFalse() + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt b/src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt new file mode 100644 index 0000000..37e3c68 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt @@ -0,0 +1,127 @@ +package com.android.tools.metalava + +import org.junit.Test + +/** Tests for the --show-annotation functionality */ +class ShowAnnotationTest : DriverTest() { + + @Test + fun `Basic showAnnotation test`() { + check( + includeSystemApiAnnotations = true, + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.SystemApi; + public class Foo { + public void method1() { } + + /** + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public void method2() { } + + /** + * @hide Always hidden + */ + public void method3() { } + + @SystemApi + public void method4() { } + + } + """ + ), + java( + """ + package foo.bar; + public class Bar { + } + """ + ), + systemApiSource + ), + + extraArguments = arrayOf( + "--hide-package", "android.annotation", + "--hide-package", "android.support.annotation" + ), + + api = """ + package test.pkg { + public class Foo { + method public void method2(); + method public void method4(); + } + } + """ + ) + } + + @Test + fun `Basic showAnnotation with showUnannotated test`() { + check( + includeSystemApiAnnotations = true, + showUnannotated = true, + checkDoclava1 = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.SystemApi; + public class Foo { + public void method1() { } + + /** + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public void method2() { } + + /** + * @hide Always hidden + */ + public void method3() { } + + @SystemApi + public void method4() { } + + } + """ + ), + java( + """ + package foo.bar; + public class Bar { + } + """ + ), + systemApiSource + ), + + extraArguments = arrayOf( + "--hide-package", "android.annotation", + "--hide-package", "android.support.annotation" + ), + + api = """ + package foo.bar { + public class Bar { + ctor public Bar(); + } + } + package test.pkg { + public class Foo { + ctor public Foo(); + method public void method1(); + method public void method2(); + method public void method4(); + } + } + """ + ) + } +} \ 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 new file mode 100644 index 0000000..215bf43 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -0,0 +1,2776 @@ +/* + * Copyright (C) 2017 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 com.android.tools.lint.checks.infrastructure.TestFile +import org.intellij.lang.annotations.Language +import org.junit.Test + +@SuppressWarnings("ALL") +class StubsTest : DriverTest() { + // TODO: test fields that need initialization + // TODO: test @DocOnly handling + + private fun checkStubs( + @Language("JAVA") source: String, + compatibilityMode: Boolean = true, + warnings: String? = "", + checkDoclava1: Boolean = false, + api: String? = null, + extraArguments: Array<String> = emptyArray(), + vararg sourceFiles: TestFile + ) { + check( + *sourceFiles, + stubs = arrayOf(source), + compatibilityMode = compatibilityMode, + warnings = warnings, + checkDoclava1 = checkDoclava1, + checkCompilation = true, + api = api, + extraArguments = extraArguments + ) + } + + @Test + fun `Generate stubs for basic class`() { + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + /** This is the documentation for the class */ + @SuppressWarnings("ALL") + public class Foo { + private int hidden; + + /** My field doc */ + protected static final String field = "a\nb\n\"test\""; + + /** + * Method documentation. + * Maybe it spans + * multiple lines. + */ + protected static void onCreate(String parameter1) { + // This is not in the stub + System.out.println(parameter1); + } + + static { + System.out.println("Not included in stub"); + } + } + """ + ) + ), + source = """ + package test.pkg; + /** This is the documentation for the class */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + /** + * Method documentation. + * Maybe it spans + * multiple lines. + */ + protected static void onCreate(java.lang.String parameter1) { throw new RuntimeException("Stub!"); } + /** My field doc */ + protected static final java.lang.String field = "a\nb\n\"test\""; + } + """ + ) + } + + @Test + fun `Generate stubs for generics`() { + // Basic interface with generics; makes sure <T extends Object> is written as just <T> + // Also include some more complex generics expressions to make sure they're serialized + // correctly (in particular, using fully qualified names instead of what appears in + // the source code.) + check( + checkDoclava1 = false, + checkCompilation = true, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface2<T extends Number> + extends MyBaseInterface { + class TtsSpan<C extends MyInterface<?>> { } + abstract class Range<T extends Comparable<? super T>> { } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public interface MyInterface<T extends Object> + extends MyBaseInterface { + } + """ + ), + java( + """ + package test.pkg; + public interface MyBaseInterface { + } + """ + ) + ), + warnings = "", + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface MyInterface2<T extends java.lang.Number> extends test.pkg.MyBaseInterface { + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class Range<T extends java.lang.Comparable<? super T>> { + public Range() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class TtsSpan<C extends test.pkg.MyInterface<?>> { + public TtsSpan() { throw new RuntimeException("Stub!"); } + } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface MyInterface<T> extends test.pkg.MyBaseInterface { + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface MyBaseInterface { + } + """ + ) + ) + } + + @Test + fun `Generate stubs for class that should not get default constructor (has other constructors)`() { + // Class without explicit constructors (shouldn't insert default constructor) + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo { + public Foo(int i) { + + } + public Foo(int i, int j) { + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo(int i) { throw new RuntimeException("Stub!"); } + public Foo(int i, int j) { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs for class that already has a private constructor`() { + // Class without private constructor; no default constructor should be inserted + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo { + private Foo() { + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + Foo() { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs for interface class`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public interface Foo { + void foo(); + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface Foo { + public void foo(); + } + """ + ) + } + + @Test + fun `Generate stubs for enum`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public enum Foo { + A, B; + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public enum Foo { + A, B; + } + """ + ) + } + + @Test + fun `Generate stubs for annotation type`() { + // Interface: makes sure the right modifiers etc are shown (and that "package private" methods + // in the interface are taken to be public etc) + checkStubs( + // For unknown reasons, doclava1 behaves differently here than when invoked on the + // whole platform + checkDoclava1 = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import static java.lang.annotation.ElementType.*; + import java.lang.annotation.*; + @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) + @Retention(RetentionPolicy.CLASS) + public @interface Foo { + String value(); + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public @interface Foo { + public java.lang.String value(); + } + """ + ) + } + + @Test + fun `Generate stubs for class with superclass`() { + // Make sure superclass statement is correct; unlike signature files, inherited method from parent + // that has same signature should be included in the child + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo extends Super { + @Override public void base() { } + public void child() { } + } + """ + ), java( + """ + package test.pkg; + public class Super { + public void base() { } + } + """ + ) + ), + source = + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo extends test.pkg.Super { + public Foo() { throw new RuntimeException("Stub!"); } + public void base() { throw new RuntimeException("Stub!"); } + public void child() { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs for fields with initial values`() { + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class Foo { + private int hidden = 1; + int hidden2 = 2; + /** @hide */ + int hidden3 = 3; + + protected int field00; // No value + public static final boolean field01 = true; + public static final int field02 = 42; + public static final long field03 = 42L; + public static final short field04 = 5; + public static final byte field05 = 5; + public static final char field06 = 'c'; + public static final float field07 = 98.5f; + public static final double field08 = 98.5; + public static final String field09 = "String with \"escapes\" and \u00a9..."; + public static final double field10 = Double.NaN; + public static final double field11 = Double.POSITIVE_INFINITY; + + public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef"; + public static final char HEX_INPUT = 61184; + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + public static final java.lang.String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef"; + public static final char HEX_INPUT = 61184; // 0xef00 '\uef00' + protected int field00; + public static final boolean field01 = true; + public static final int field02 = 42; // 0x2a + public static final long field03 = 42L; // 0x2aL + public static final short field04 = 5; // 0x5 + public static final byte field05 = 5; // 0x5 + public static final char field06 = 99; // 0x0063 'c' + public static final float field07 = 98.5f; + public static final double field08 = 98.5; + public static final java.lang.String field09 = "String with \"escapes\" and \u00a9..."; + public static final double field10 = (0.0/0.0); + public static final double field11 = (1.0/0.0); + } + """ + ) + } + + @Test + fun `Generate stubs for various modifier scenarios`() { + // Include as many modifiers as possible to see which ones are included + // in the signature files, and the expected sorting order. + // Note that the signature files treat "deprecated" as a fake modifier. + // Note also how the "protected" modifier on the interface method gets + // promoted to public. + checkStubs( + warnings = null, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public abstract class Foo { + @Deprecated private static final long field1 = 5; + @Deprecated private static volatile long field2 = 5; + @Deprecated public static strictfp final synchronized void method1() { } + @Deprecated public static final synchronized native void method2(); + @Deprecated protected static final class Inner1 { } + @Deprecated protected static abstract class Inner2 { } + @Deprecated protected interface Inner3 { + protected default void method3() { } + static void method4() { } + } + } + """ + ) + ), + + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + @Deprecated public static final synchronized strictfp void method1() { throw new RuntimeException("Stub!"); } + @Deprecated public static final synchronized native void method2(); + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @Deprecated protected static final class Inner1 { + protected Inner1() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @Deprecated protected abstract static class Inner2 { + protected Inner2() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @Deprecated protected static interface Inner3 { + public default void method3() { throw new RuntimeException("Stub!"); } + public static void method4() { throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + @Test + fun `Generate stubs for class with abstract enum methods`() { + // As per https://bugs.openjdk.java.net/browse/JDK-6287639 + // abstract methods in enums should not be listed as abstract, + // but doclava1 does, so replicate this. + // Also checks that we handle both enum fields and regular fields + // and that they are listed separately. + + checkStubs( + checkDoclava1 = false, // Doclava1 does not generate compileable source for this + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public enum FooBar { + ABC { + @Override + protected void foo() { } + }, DEF { + @Override + protected void foo() { } + }; + + protected abstract void foo(); + public static int field1 = 1; + public int field2 = 2; + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public enum FooBar { + ABC, DEF; + protected void foo() { throw new RuntimeException("Stub!"); } + public static int field1 = 1; // 0x1 + public int field2 = 2; // 0x2 + } + """ + ) + } + + @Test + fun `Check erasure in throws list`() { + // Makes sure that when we have a generic signature in the throws list we take + // the erasure instead (in compat mode); "Throwable" instead of "X" in the below + // test. Real world example: Optional.orElseThrow. + checkStubs( + compatibilityMode = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import java.util.function.Supplier; + + @SuppressWarnings("RedundantThrows") + public final class Test<T> { + public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X { + return null; + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public final class Test<T> { + public Test() { throw new RuntimeException("Stub!"); } + public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X> exceptionSupplier) throws java.lang.Throwable { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs for additional generics scenarios`() { + // Some additional declarations where PSI default type handling diffs from doclava1 + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public abstract class Collections { + public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T> collection) { + return null; + } + public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T t); + public final class Range<T extends java.lang.Comparable<? super T>> { } + } + """ + ) + ), + + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class Collections { + public Collections() { throw new RuntimeException("Stub!"); } + public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T> collection) { throw new RuntimeException("Stub!"); } + public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T t); + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public final class Range<T extends java.lang.Comparable<? super T>> { + public Range() { throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + @Test + fun `Generate stubs for even more generics scenarios`() { + // Some additional declarations where PSI default type handling diffs from doclava1 + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import java.util.Set; + + @SuppressWarnings("ALL") + public class MoreAsserts { + public static void assertEquals(String arg0, Set<? extends Object> arg1, Set<? extends Object> arg2) { } + public static void assertEquals(Set<? extends Object> arg1, Set<? extends Object> arg2) { } + } + """ + ) + ), + + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MoreAsserts { + public MoreAsserts() { throw new RuntimeException("Stub!"); } + public static void assertEquals(java.lang.String arg0, java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); } + public static void assertEquals(java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs enum instance methods`() { + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public enum ChronUnit implements TempUnit { + C(1), B(2), A(3); + + ChronUnit(int y) { + } + + public String valueOf(int x) { + return Integer.toString(x + 5); + } + + public String values(String separator) { + return null; + } + + @Override + public String toString() { + return name(); + } + } + """ + ), java( + """ + package test.pkg; + + public interface TempUnit { + @Override + String toString(); + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public enum ChronUnit implements test.pkg.TempUnit { + C, B, A; + public java.lang.String valueOf(int x) { throw new RuntimeException("Stub!"); } + public java.lang.String toString() { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Generate stubs with superclass filtering`() { + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyClass extends HiddenParent { + public void method4() { } + } + """ + ), java( + """ + package test.pkg; + /** @hide */ + @SuppressWarnings("ALL") + public class HiddenParent extends HiddenParent2 { + public static final String CONSTANT = "MyConstant"; + protected int mContext; + public void method3() { } + } + """ + ), java( + """ + package test.pkg; + /** @hide */ + @SuppressWarnings("ALL") + public class HiddenParent2 extends PublicParent { + public void method2() { } + } + """ + ), java( + """ + package test.pkg; + public class PublicParent { + public void method1() { } + } + """ + ) + ), + // Notice how the intermediate methods (method2, method3) have been removed + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass extends test.pkg.PublicParent { + public MyClass() { throw new RuntimeException("Stub!"); } + public void method4() { throw new RuntimeException("Stub!"); } + } + """, + warnings = "src/test/pkg/MyClass.java:2: warning: Public class test.pkg.MyClass stripped of unavailable superclass test.pkg.HiddenParent [HiddenSuperclass:111]" + ) + } + + @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. + //checkDoclava1 = true, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + public class MyClass extends HiddenParent { + public void method1() { } + } + """ + ), java( + """ + package test.pkg; + class HiddenParent { + public static final String CONSTANT = "MyConstant"; + protected int mContext; + public void method2() { } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass { + public MyClass() { throw new RuntimeException("Stub!"); } + public void method1() { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Check implementing a package private interface`() { + // If you implement a package private interface, we just remove it and inline the members into + // the subclass + + // BUG: Note that we need to implement the parent + checkStubs( + compatibilityMode = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyClass implements HiddenInterface { + @Override public void method() { } + @Override public void other() { } + } + """ + ), java( + """ + package test.pkg; + public interface OtherInterface { + void other(); + } + """ + ), java( + """ + package test.pkg; + interface HiddenInterface extends OtherInterface { + void method() { } + String CONSTANT = "MyConstant"; + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass implements test.pkg.OtherInterface { + public MyClass() { throw new RuntimeException("Stub!"); } + public void method() { throw new RuntimeException("Stub!"); } + public void other() { throw new RuntimeException("Stub!"); } + public static final java.lang.String CONSTANT = "MyConstant"; + } + """ + ) + } + + @Test + fun `Check throws list`() { + // Make sure we format a throws list + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import java.io.IOException; + + @SuppressWarnings("RedundantThrows") + public abstract class AbstractCursor { + @Override protected void finalize1() throws Throwable { } + @Override protected void finalize2() throws IOException, IllegalArgumentException { } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class AbstractCursor { + public AbstractCursor() { throw new RuntimeException("Stub!"); } + protected void finalize1() throws java.lang.Throwable { throw new RuntimeException("Stub!"); } + protected void finalize2() throws java.io.IOException, java.lang.IllegalArgumentException { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Check generating constants in interface without inline-able initializers`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + public interface MyClass { + String[] CONSTANT1 = {"MyConstant","MyConstant2"}; + boolean CONSTANT2 = Boolean.getBoolean(System.getenv("VAR1")); + int CONSTANT3 = Integer.parseInt(System.getenv("VAR2")); + String CONSTANT4 = null; + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface MyClass { + public static final java.lang.String[] CONSTANT1 = null; + public static final boolean CONSTANT2 = false; + public static final int CONSTANT3 = 0; // 0x0 + public static final java.lang.String CONSTANT4 = null; + } + """ + ) + } + + @Test + fun `Handle non-constant fields in final classes`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("all") + public class FinalFieldTest { + public interface TemporalField { + String getBaseUnit(); + } + public static final class IsoFields { + public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER; + private IsoFields() { + throw new AssertionError("Not instantiable"); + } + + private static enum Field implements TemporalField { + DAY_OF_QUARTER { + @Override + public String getBaseUnit() { + return "days"; + } + } + }; + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class FinalFieldTest { + public FinalFieldTest() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static final class IsoFields { + IsoFields() { throw new RuntimeException("Stub!"); } + public static final test.pkg.FinalFieldTest.TemporalField DAY_OF_QUARTER; + static { DAY_OF_QUARTER = null; } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface TemporalField { + public java.lang.String getBaseUnit(); + } + } + """ + ) + } + + @Test + fun `Test final instance fields`() { + // Instance fields in a class must be initialized + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("all") + public class InstanceFieldTest { + public static final class WindowLayout { + public WindowLayout(int width, int height, int gravity) { + this.width = width; + this.height = height; + this.gravity = gravity; + } + + public final int width; + public final int height; + public final int gravity; + + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class InstanceFieldTest { + public InstanceFieldTest() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static final class WindowLayout { + public WindowLayout(int width, int height, int gravity) { throw new RuntimeException("Stub!"); } + public final int gravity; + { gravity = 0; } + public final int height; + { height = 0; } + public final int width; + { width = 0; } + } + } + """ + ) + } + + @Test + fun `Check generating constants in class without inline-able initializers`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + public class MyClass { + public static String[] CONSTANT1 = {"MyConstant","MyConstant2"}; + public static boolean CONSTANT2 = Boolean.getBoolean(System.getenv("VAR1")); + public static int CONSTANT3 = Integer.parseInt(System.getenv("VAR2")); + public static String CONSTANT4 = null; + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass { + public MyClass() { throw new RuntimeException("Stub!"); } + public static java.lang.String[] CONSTANT1; + public static boolean CONSTANT2; + public static int CONSTANT3; + public static java.lang.String CONSTANT4; + } + """ + ) + } + + @Test + fun `Check generating annotation source`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package android.view.View; + import android.annotation.IntDef; + import android.annotation.IntRange; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + public class View { + @SuppressWarnings("all") + public static class MeasureSpec { + private static final int MODE_SHIFT = 30; + private static final int MODE_MASK = 0x3 << MODE_SHIFT; + /** @hide */ + @SuppressWarnings("all") + @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) + @Retention(RetentionPolicy.SOURCE) + public @interface MeasureSpecMode {} + public static final int UNSPECIFIED = 0 << MODE_SHIFT; + public static final int EXACTLY = 1 << MODE_SHIFT; + public static final int AT_MOST = 2 << MODE_SHIFT; + + public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, + @MeasureSpecMode int mode) { + return 0; + } + } + } + """ + ), + intDefAnnotationSource, + intRangeAnnotationSource + ), + warnings = "", + source = """ + package android.view.View; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class View { + public View() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class MeasureSpec { + public MeasureSpec() { throw new RuntimeException("Stub!"); } + /** + * @param size Value is between 0 and (1 << MeasureSpec.MODE_SHIFT) - 1 inclusive + * @param mode Value is {@link android.view.View.View.MeasureSpec#UNSPECIFIED}, {@link android.view.View.View.MeasureSpec#EXACTLY}, or {@link android.view.View.View.MeasureSpec#AT_MOST} + */ + public static int makeMeasureSpec(@android.support.annotation.IntRange(from=0, to=0x40000000 - 1) int size, int mode) { throw new RuntimeException("Stub!"); } + public static final int AT_MOST = -2147483648; // 0x80000000 + public static final int EXACTLY = 1073741824; // 0x40000000 + public static final int UNSPECIFIED = 0; // 0x0 + } + } + """ + ) + } + + @Test + fun `Check generating classes with generics`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + public class Generics { + public <T> Generics(int surfaceSize, Class<T> klass) { + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics { + public <T> Generics(int surfaceSize, java.lang.Class<T> klass) { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Check generating annotation for hidden constants`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + import android.content.Intent; + import android.annotation.RequiresPermission; + + public abstract class HiddenPermission { + @RequiresPermission(allOf = { + android.Manifest.permission.INTERACT_ACROSS_USERS, + android.Manifest.permission.BROADCAST_STICKY + }) + public abstract void removeStickyBroadcast(@RequiresPermission Object intent); + } + """ + ), + java( + """ + package android; + + public final class Manifest { + @SuppressWarnings("JavaDoc") + public static final class permission { + public static final String BROADCAST_STICKY = "android.permission.BROADCAST_STICKY"; + /** @SystemApi @hide Allows an application to call APIs that allow it to do interactions + across the users on the device, using singleton services and + user-targeted broadcasts. This permission is not available to + third party applications. */ + public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS"; + } + } + """ + ), + requiresPermissionSource + ), + warnings = "src/test/pkg/HiddenPermission.java:5: lint: Permission android.Manifest.permission.INTERACT_ACROSS_USERS required by method test.pkg.HiddenPermission.removeStickyBroadcast(Object) is hidden or removed [MissingPermission:132]", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class HiddenPermission { + public HiddenPermission() { throw new RuntimeException("Stub!"); } + /** + * Requires {@link android.Manifest.permission#INTERACT_ACROSS_USERS} and {@link android.Manifest.permission#BROADCAST_STICKY} + */ + @android.support.annotation.RequiresPermission(allOf={"android.permission.INTERACT_ACROSS_USERS", android.Manifest.permission.BROADCAST_STICKY}) public abstract void removeStickyBroadcast(@android.support.annotation.RequiresPermission java.lang.Object intent); + } + """ + ) + } + + @Test + fun `Check generating type parameters in interface list`() { + // In signature files we don't include generics in the interface list. + // In stubs, we do. + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("NullableProblems") + public class GenericsInInterfaces<T> implements Comparable<GenericsInInterfaces> { + @Override + public int compareTo(GenericsInInterfaces o) { + return 0; + } + + void foo(T bar) { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class GenericsInInterfaces<T> implements java.lang.Comparable { + ctor public GenericsInInterfaces(); + method public int compareTo(test.pkg.GenericsInInterfaces); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class GenericsInInterfaces<T> implements java.lang.Comparable<test.pkg.GenericsInInterfaces> { + public GenericsInInterfaces() { throw new RuntimeException("Stub!"); } + public int compareTo(test.pkg.GenericsInInterfaces o) { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Preserve file header comments`() { + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + /* + My header 1 + */ + + /* + My header 2 + */ + + // My third comment + + package test.pkg; + + public class HeaderComments { + } + """ + ) + ), + source = """ + /* + My header 1 + */ + /* + My header 2 + */ + // My third comment + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class HeaderComments { + public HeaderComments() { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Basic Kotlin class`() { + checkStubs( + sourceFiles = *arrayOf( + kotlin( + """ + /* My file header */ + // Another comment + @file:JvmName("Driver") + package test.pkg + /** My class doc */ + class Kotlin(val property1: String = "Default Value", arg2: Int) : Parent() { + override fun method() = "Hello World" + /** My method doc */ + fun otherMethod(ok: Boolean, times: Int) { + } + + /** property doc */ + var property2: String? = null + + /** @hide */ + var hiddenProperty: String? = "hidden" + + private var someField = 42 + @JvmField + var someField2 = 42 + } + + open class Parent { + open fun method(): String? = null + open fun method2(value1: Boolean, value2: Boolean?): String? = null + open fun method3(value1: Int?, value2: Int): Int = null + } + """ + ) + ), + source = """ + /* My file header */ + // Another comment + package test.pkg; + /** My class doc */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public final class Kotlin extends test.pkg.Parent { + 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!"); } + /** property doc */ + @android.support.annotation.Nullable public final 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 int someField2; + } + """, + checkDoclava1 = false /* doesn't support Kotlin... */ + ) + } + + @Test + fun `Parameter Names in Java`() { + // Java code which explicitly specifies parameter names: make sure stub uses + // parameter name + checkStubs( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.support.annotation.ParameterName; + + public class Foo { + public void foo(int javaParameter1, @ParameterName("publicParameterName") int javaParameter2) { + } + } + """ + ), + supportParameterName + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + public void foo(int javaParameter1, int publicParameterName) { throw new RuntimeException("Stub!"); } + } + """, + checkDoclava1 = false /* doesn't support parameter names */ + ) + } + + @Test + fun `Remove Hidden Annotations`() { + // When APIs reference annotations that are hidden, make sure the're excluded from the stubs and + // signature files + checkStubs( + compatibilityMode = false, + checkDoclava1 = false, // doesn't support type-use annotations + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + public class Foo { + public void foo(int p1, @MyAnnotation("publicParameterName") java.util.Map<String, @MyAnnotation("Something") String> p2) { + } + } + """ + ), + java( + """ + package test.pkg; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + /** @hide */ + @SuppressWarnings("WeakerAccess") + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + public @interface MyAnnotation { + String value(); + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(); + method public void foo(int, java.util.Map<String, java.lang.String>!); + } + } + """, + + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Foo { + public Foo() { throw new RuntimeException("Stub!"); } + public void foo(int p1, java.util.Map<java.lang.String, java.lang.String> p2) { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + @Test + fun `Arguments to super constructors`() { + // When overriding constructors we have to supply arguments + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("WeakerAccess") + public class Constructors { + public class Parent { + public Parent(String s, int i, long l, boolean b, short sh) { + } + } + + public class Child extends Parent { + public Child(String s, int i, long l, boolean b, short sh) { + super(s, i, l, b, sh); + } + + private Child(String s) { + super(s, 0, 0, false, 0); + } + } + + public class Child2 extends Parent { + Child2(String s) { + super(s, 0, 0, false, 0); + } + } + + public class Child3 extends Child2 { + private Child3(String s) { + super("something"); + } + } + + public class Child4 extends Parent { + Child4(String s, HiddenClass hidden) { + super(s, 0, 0, true, 0); + } + } + /** @hide */ + public class HiddenClass { + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Constructors { + public Constructors() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child extends test.pkg.Constructors.Parent { + public Child(java.lang.String s, int i, long l, boolean b, short sh) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child2 extends test.pkg.Constructors.Parent { + Child2(java.lang.String s) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child3 extends test.pkg.Constructors.Child2 { + Child3(java.lang.String s) { super(null); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child4 extends test.pkg.Constructors.Parent { + Child4() { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Parent { + public Parent(java.lang.String s, int i, long l, boolean b, short sh) { throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + // TODO: Add test to see what happens if I have Child4 in a different package which can't access the package private constructor of child3? + + @Test + fun `DocOnly members should be omitted`() { + // When marked @doconly don't include in stubs or signature files + // unless specifically asked for (which we do when generating docs). + checkStubs( + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("JavaDoc") + public class Outer { + /** @doconly Some docs here */ + public class MyClass1 { + public int myField; + } + + public class MyClass2 { + /** @doconly Some docs here */ + public int myField; + + /** @doconly Some docs here */ + public int myMethod() { return 0; } + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Outer { + public Outer() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass2 { + public MyClass2() { throw new RuntimeException("Stub!"); } + } + } + """, + api = """ + package test.pkg { + public class Outer { + ctor public Outer(); + } + public class Outer.MyClass2 { + ctor public Outer.MyClass2(); + } + } + """ + ) + } + + @Test + fun `DocOnly members should be included when requested`() { + // When marked @doconly don't include in stubs or signature files + // unless specifically asked for (which we do when generating docs). + checkStubs( + extraArguments = arrayOf("--include-doconly"), + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("JavaDoc") + public class Outer { + /** @doconly Some docs here */ + public class MyClass1 { + public int myField; + } + + public class MyClass2 { + /** @doconly Some docs here */ + public int myField; + + /** @doconly Some docs here */ + public int myMethod() { return 0; } + } + } + """ + ) + ), + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Outer { + public Outer() { throw new RuntimeException("Stub!"); } + /** @doconly Some docs here */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass1 { + public MyClass1() { throw new RuntimeException("Stub!"); } + public int myField; + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass2 { + public MyClass2() { throw new RuntimeException("Stub!"); } + /** @doconly Some docs here */ + public int myMethod() { throw new RuntimeException("Stub!"); } + /** @doconly Some docs here */ + public int myField; + } + } + """ + ) + } + + @Test + fun `Check generating required stubs from hidden super classes and interfaces`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + public class MyClass extends HiddenSuperClass implements HiddenInterface, PublicInterface2 { + public void myMethod() { } + @Override public void publicInterfaceMethod2() { } + } + """ + ), + java( + """ + package test.pkg; + class HiddenSuperClass extends PublicSuperParent { + @Override public void inheritedMethod2() { } + @Override public void publicInterfaceMethod() { } + @Override public void publicMethod() {} + @Override public void publicMethod2() {} + } + """ + ), + java( + """ + package test.pkg; + public abstract class PublicSuperParent { + public void inheritedMethod1() {} + public void inheritedMethod2() {} + public abstract void publicMethod() {} + } + """ + ), + java( + """ + package test.pkg; + interface HiddenInterface extends PublicInterface { + int MY_CONSTANT = 5; + void hiddenInterfaceMethod(); + } + """ + ), + java( + """ + package test.pkg; + public interface PublicInterface { + void publicInterfaceMethod(); + } + """ + ), + java( + """ + package test.pkg; + public interface PublicInterface2 { + void publicInterfaceMethod2(); + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class MyClass extends test.pkg.PublicSuperParent implements test.pkg.PublicInterface test.pkg.PublicInterface2 { + ctor public MyClass(); + method public void myMethod(); + method public void publicInterfaceMethod2(); + field public static final int MY_CONSTANT = 5; // 0x5 + } + public abstract interface PublicInterface { + method public abstract void publicInterfaceMethod(); + } + public abstract interface PublicInterface2 { + method public abstract void publicInterfaceMethod2(); + } + public abstract class PublicSuperParent { + ctor public PublicSuperParent(); + method public void inheritedMethod1(); + method public void inheritedMethod2(); + method public abstract void publicMethod(); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass extends test.pkg.PublicSuperParent implements test.pkg.PublicInterface, test.pkg.PublicInterface2 { + 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 publicInterfaceMethod() { throw new RuntimeException("Stub!"); } + public static final int MY_CONSTANT = 5; // 0x5 + } + """ + ) + } + + @Test + fun `Rewrite libcore annotations`() { + check( + checkDoclava1 = false, + checkCompilation = true, + sourceFiles = *arrayOf( + java( + "package my.pkg;\n" + + "public class String {\n" + + "public String(char @libcore.util.NonNull [] value) { throw new RuntimeException(\"Stub!\"); }\n" + + "}\n" + ) + ), + warnings = "", + api = """ + package my.pkg { + public class String { + ctor public String(char[]); + } + } + """, + stubs = arrayOf( + """ + package my.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class String { + public String(char @android.support.annotation.NonNull [] value) { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Test inaccessible constructors`() { + // If the constructors of a class are not visible, and the class has subclasses, + // those subclass stubs will need to reference these inaccessible constructors. + // This generally only happens when the constructors are package private (and + // therefore hidden) but the subclass using it is also in the same package. + + check( + checkDoclava1 = false, + checkCompilation = true, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + public class MyClass1 { + MyClass1(int myVar) { } + } + """ + ), + java( + """ + package test.pkg; + import java.io.IOException; + @SuppressWarnings("RedundantThrows") + public class MySubClass1 extends MyClass1 { + MySubClass1(int myVar) throws IOException { super(myVar); } + } + """ + ), + java( + """ + package test.pkg; + public class MyClass2 { + /** @hide */ + public MyClass2(int myVar) { } + } + """ + ), + java( + """ + package test.pkg; + public class MySubClass2 extends MyClass2 { + public MySubClass2() { super(5); } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class MyClass1 { + } + public class MyClass2 { + } + public class MySubClass1 extends test.pkg.MyClass1 { + } + public class MySubClass2 extends test.pkg.MyClass2 { + ctor public MySubClass2(); + } + } + """, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass1 { + MyClass1(int myVar) { throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MySubClass1 extends test.pkg.MyClass1 { + MySubClass1(int myVar) throws java.io.IOException { super(0); throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass2 { + /** @hide */ + MyClass2(int myVar) { throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MySubClass2 extends test.pkg.MyClass2 { + public MySubClass2() { super(0); throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + @Test + fun `Generics Variable Rewriting`() { + // When we move methods from hidden superclasses into the subclass since they + // provide the implementation for a required method, it's possible that the + // method we copied in is referencing generics with a different variable than + // in the current class, so we need to handle this + + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + // TODO: Try using prefixes like "A", and "AA" to make sure my generics + // variable renaming doesn't do something really dumb + java( + """ + package test.pkg; + + import java.util.List; + import java.util.Map; + + public class Generics { + public class MyClass<X extends Number,Y> extends HiddenParent<X,Y> implements PublicParent<X,Y> { + } + + public class MyClass2<W> extends HiddenParent<Float,W> implements PublicParent<Float, W> { + } + + public class MyClass3 extends HiddenParent<Float,Double> implements PublicParent<Float,Double> { + } + + class HiddenParent<M, N> extends HiddenParent2<M, N> { + } + + class HiddenParent2<T, TT> { + public Map<T,Map<TT, String>> createMap(List<T> list) { + return null; + } + } + + public interface PublicParent<A extends Number,B> { + Map<A,Map<B, String>> createMap(List<A> list); + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics { + public Generics() { throw new RuntimeException("Stub!"); } + @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"}) + public static interface PublicParent<A extends java.lang.Number, B> { + public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A> list); + } + } + """ + ) + } + + @Test + fun `Rewriting type parameters in interfaces from hidden super classes and in throws lists`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + import java.io.IOException; + import java.util.List; + import java.util.Map; + + @SuppressWarnings({"RedundantThrows", "WeakerAccess"}) + public class Generics { + public class MyClass<X, Y extends Number> extends HiddenParent<X, Y> implements PublicInterface<X, Y> { + } + + class HiddenParent<M, N extends Number> extends PublicParent<M, N> { + public Map<M, Map<N, String>> createMap(List<M> list) throws MyThrowable { + return null; + } + + protected List<M> foo() { + return null; + } + + } + + class MyThrowable extends IOException { + } + + public abstract class PublicParent<A, B extends Number> { + protected abstract List<A> foo(); + } + + public interface PublicInterface<A, B> { + Map<A, Map<B, String>> createMap(List<A> list) throws IOException; + } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class Generics { + ctor public Generics(); + } + public class Generics.MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent implements test.pkg.Generics.PublicInterface { + ctor public Generics.MyClass(); + } + 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; + } + public abstract class Generics.PublicParent<A, B extends java.lang.Number> { + ctor public Generics.PublicParent(); + method protected abstract java.util.List<A> foo(); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics { + public Generics() { throw new RuntimeException("Stub!"); } + @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"}) + public static interface PublicInterface<A, B> { + public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A> list) throws java.io.IOException; + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class PublicParent<A, B extends java.lang.Number> { + public PublicParent() { throw new RuntimeException("Stub!"); } + protected abstract java.util.List<A> foo(); + } + } + """ + ) + } + + @Test + fun `Rewriting implements class references`() { + // Checks some more subtle bugs around generics type variable renaming + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + import java.util.Collection; + import java.util.Set; + + @SuppressWarnings("all") + public class ConcurrentHashMap<K, V> { + public abstract static class KeySetView<K, V> extends CollectionView<K, V, K> + implements Set<K>, java.io.Serializable { + } + + abstract static class CollectionView<K, V, E> + implements Collection<E>, java.io.Serializable { + public final Object[] toArray() { return null; } + + public final <T> T[] toArray(T[] a) { + return null; + } + + @Override + public int size() { + return 0; + } + } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class ConcurrentHashMap<K, V> { + ctor public ConcurrentHashMap(); + } + public static abstract class ConcurrentHashMap.KeySetView<K, V> implements java.util.Collection java.io.Serializable java.util.Set { + ctor public ConcurrentHashMap.KeySetView(); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class ConcurrentHashMap<K, V> { + public ConcurrentHashMap() { throw new RuntimeException("Stub!"); } + @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!"); } + } + } + """ + ) + } + + @Test + fun `Arrays in type arguments`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + public class Generics2 { + public class FloatArrayEvaluator implements TypeEvaluator<float[]> { + } + + @SuppressWarnings("WeakerAccess") + public interface TypeEvaluator<T> { + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics2 { + public Generics2() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class FloatArrayEvaluator implements test.pkg.Generics2.TypeEvaluator<float[]> { + public FloatArrayEvaluator() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface TypeEvaluator<T> { + } + } + """ + ) + } + + @Test + fun `Interface extending multiple interfaces`() { + // Ensure that we handle sorting correctly where we're mixing super classes and implementing + // interfaces + // Real-world example: XmlResourceParser + check( + checkDoclava1 = false, + checkCompilation = true, + sourceFiles = *arrayOf( + java( + """ + package android.content.res; + import android.util.AttributeSet; + import org.xmlpull.v1.XmlPullParser; + + public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { + public void close(); + } + """ + ), + java( + """ + package android.util; + public interface AttributeSet { + } + """ + ), + java( + """ + package java.lang; + public interface AutoCloseable { + } + """ + ), + java( + """ + package org.xmlpull.v1; + public interface XmlPullParser { + } + """ + ) + ), + stubs = arrayOf( + """ + package android.content.res; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public interface XmlResourceParser extends org.xmlpull.v1.XmlPullParser, android.util.AttributeSet, java.lang.AutoCloseable { + public void close(); + } + """ + ) + ) + } + + // TODO: Add a protected constructor too to make sure my code to make non-public constructors package private + // don't accidentally demote protected constructors to package private! + + @Test + fun `Picking Super Constructors`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + import java.io.IOException; + + @SuppressWarnings({"RedundantThrows", "JavaDoc", "WeakerAccess"}) + public class PickConstructors { + public abstract static class FileInputStream extends InputStream { + + public FileInputStream(String name) throws FileNotFoundException { + } + + public FileInputStream(File file) throws FileNotFoundException { + } + + public FileInputStream(FileDescriptor fdObj) { + this(fdObj, false /* isFdOwner */); + } + + /** + * @hide + */ + public FileInputStream(FileDescriptor fdObj, boolean isFdOwner) { + } + } + + public abstract static class AutoCloseInputStream extends FileInputStream { + public AutoCloseInputStream(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + } + } + + abstract static class HiddenParentStream extends FileInputStream { + public HiddenParentStream(FileDescriptor pfd) { + super(pfd); + } + } + + public abstract static class AutoCloseInputStream2 extends HiddenParentStream { + public AutoCloseInputStream2(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + } + } + + public abstract class ParcelFileDescriptor implements Closeable { + public abstract FileDescriptor getFileDescriptor(); + } + + public static interface Closeable extends AutoCloseable { + } + + public static interface AutoCloseable { + } + + public static abstract class InputStream implements Closeable { + } + + public static class File { + } + + public static final class FileDescriptor { + } + + public static class FileNotFoundException extends IOException { + } + + public static class IOException extends Exception { + } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class PickConstructors { + ctor public PickConstructors(); + } + public static abstract class PickConstructors.AutoCloseInputStream extends test.pkg.PickConstructors.FileInputStream { + ctor public PickConstructors.AutoCloseInputStream(test.pkg.PickConstructors.ParcelFileDescriptor); + } + public static abstract class PickConstructors.AutoCloseInputStream2 extends test.pkg.PickConstructors.FileInputStream { + ctor public PickConstructors.AutoCloseInputStream2(test.pkg.PickConstructors.ParcelFileDescriptor); + } + public static abstract interface PickConstructors.AutoCloseable { + } + public static abstract interface PickConstructors.Closeable implements test.pkg.PickConstructors.AutoCloseable { + } + public static class PickConstructors.File { + ctor public PickConstructors.File(); + } + public static final class PickConstructors.FileDescriptor { + ctor public PickConstructors.FileDescriptor(); + } + public static abstract class PickConstructors.FileInputStream extends test.pkg.PickConstructors.InputStream { + ctor public PickConstructors.FileInputStream(java.lang.String) throws test.pkg.PickConstructors.FileNotFoundException; + ctor public PickConstructors.FileInputStream(test.pkg.PickConstructors.File) throws test.pkg.PickConstructors.FileNotFoundException; + ctor public PickConstructors.FileInputStream(test.pkg.PickConstructors.FileDescriptor); + } + public static class PickConstructors.FileNotFoundException extends test.pkg.PickConstructors.IOException { + ctor public PickConstructors.FileNotFoundException(); + } + public static class PickConstructors.IOException extends java.lang.Exception { + ctor public PickConstructors.IOException(); + } + public static abstract class PickConstructors.InputStream implements test.pkg.PickConstructors.Closeable { + ctor public PickConstructors.InputStream(); + } + public abstract class PickConstructors.ParcelFileDescriptor implements test.pkg.PickConstructors.Closeable { + ctor public PickConstructors.ParcelFileDescriptor(); + method public abstract test.pkg.PickConstructors.FileDescriptor getFileDescriptor(); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class PickConstructors { + public PickConstructors() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class AutoCloseInputStream extends test.pkg.PickConstructors.FileInputStream { + public AutoCloseInputStream(test.pkg.PickConstructors.ParcelFileDescriptor pfd) { super((test.pkg.PickConstructors.FileDescriptor)null); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class AutoCloseInputStream2 extends test.pkg.PickConstructors.FileInputStream { + public AutoCloseInputStream2(test.pkg.PickConstructors.ParcelFileDescriptor pfd) { super((test.pkg.PickConstructors.FileDescriptor)null); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface AutoCloseable { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface Closeable extends test.pkg.PickConstructors.AutoCloseable { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class File { + public File() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static final class FileDescriptor { + public FileDescriptor() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class FileInputStream extends test.pkg.PickConstructors.InputStream { + public FileInputStream(java.lang.String name) throws test.pkg.PickConstructors.FileNotFoundException { throw new RuntimeException("Stub!"); } + public FileInputStream(test.pkg.PickConstructors.File file) throws test.pkg.PickConstructors.FileNotFoundException { throw new RuntimeException("Stub!"); } + public FileInputStream(test.pkg.PickConstructors.FileDescriptor fdObj) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class FileNotFoundException extends test.pkg.PickConstructors.IOException { + public FileNotFoundException() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class IOException extends java.lang.Exception { + public IOException() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class InputStream implements test.pkg.PickConstructors.Closeable { + public InputStream() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class ParcelFileDescriptor implements test.pkg.PickConstructors.Closeable { + public ParcelFileDescriptor() { throw new RuntimeException("Stub!"); } + public abstract test.pkg.PickConstructors.FileDescriptor getFileDescriptor(); + } + } + """ + ) + } + + @Test + fun `Picking Constructors`() { + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings({"WeakerAccess", "unused"}) + public class Constructors2 { + public class TestSuite implements Test { + + public TestSuite() { + } + + public TestSuite(final Class<?> theClass) { + } + + public TestSuite(Class<? extends TestCase> theClass, String name) { + this(theClass); + } + + public TestSuite(String name) { + } + public TestSuite(Class<?>... classes) { + } + + public TestSuite(Class<? extends TestCase>[] classes, String name) { + this(classes); + } + } + + public class TestCase { + } + + public interface Test { + } + + public class Parent { + public Parent(int x) throws IOException { + } + } + + class Intermediate extends Parent { + Intermediate(int x) throws IOException { super(x); } + } + + public class Child extends Intermediate { + public Child() throws IOException { super(5); } + public Child(float x) throws IOException { this(); } + } + + // ---------------------------------------------------- + + public abstract class DrawableWrapper { + public DrawableWrapper(Drawable dr) { + } + + DrawableWrapper(Clipstate state, Object resources) { + } + } + + + public class ClipDrawable extends DrawableWrapper { + ClipDrawable() { + this(null); + } + + public ClipDrawable(Drawable drawable, int gravity, int orientation) { this(null); } + + private ClipDrawable(Clipstate clipstate) { + super(clipstate, null); + } + } + + public class Drawable { + } + + class Clipstate { + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Constructors2 { + public Constructors2() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child extends test.pkg.Constructors2.Parent { + public Child() { super(0); throw new RuntimeException("Stub!"); } + public Child(float x) { super(0); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class ClipDrawable extends test.pkg.Constructors2.DrawableWrapper { + public ClipDrawable(test.pkg.Constructors2.Drawable drawable, int gravity, int orientation) { super(null); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Drawable { + public Drawable() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class DrawableWrapper { + public DrawableWrapper(test.pkg.Constructors2.Drawable dr) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Parent { + public Parent(int x) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface Test { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class TestCase { + public TestCase() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class TestSuite implements test.pkg.Constructors2.Test { + public TestSuite() { throw new RuntimeException("Stub!"); } + public TestSuite(java.lang.Class<?> theClass) { throw new RuntimeException("Stub!"); } + public TestSuite(java.lang.Class<? extends test.pkg.Constructors2.TestCase> theClass, java.lang.String name) { throw new RuntimeException("Stub!"); } + public TestSuite(java.lang.String name) { throw new RuntimeException("Stub!"); } + public TestSuite(java.lang.Class<?>... classes) { throw new RuntimeException("Stub!"); } + public TestSuite(java.lang.Class<? extends test.pkg.Constructors2.TestCase>[] classes, java.lang.String name) { throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + @Test + fun `Another Constructor Test`() { + // A specific scenario triggered in the API where the right super class detector was not chosen + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings({"RedundantThrows", "JavaDoc", "WeakerAccess"}) + public class PickConstructors2 { + public interface EventListener { + } + + public interface PropertyChangeListener extends EventListener { + } + + public static abstract class EventListenerProxy<T extends EventListener> + implements EventListener { + public EventListenerProxy(T listener) { + } + } + + public static class PropertyChangeListenerProxy + extends EventListenerProxy<PropertyChangeListener> + implements PropertyChangeListener { + public PropertyChangeListenerProxy(String propertyName, PropertyChangeListener listener) { + super(listener); + } + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class PickConstructors2 { + public PickConstructors2() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface EventListener { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class EventListenerProxy<T extends test.pkg.PickConstructors2.EventListener> implements test.pkg.PickConstructors2.EventListener { + public EventListenerProxy(T listener) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface PropertyChangeListener extends test.pkg.PickConstructors2.EventListener { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class PropertyChangeListenerProxy extends test.pkg.PickConstructors2.EventListenerProxy<test.pkg.PickConstructors2.PropertyChangeListener> implements test.pkg.PickConstructors2.PropertyChangeListener { + public PropertyChangeListenerProxy(java.lang.String propertyName, test.pkg.PickConstructors2.PropertyChangeListener listener) { super(null); throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + @Test + fun `Overriding protected methods`() { + // Checks a scenario where the stubs were missing overrides + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("all") + public class Layouts { + public static class View { + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + } + } + + public static abstract class ViewGroup extends View { + @Override + protected abstract void onLayout(boolean changed, + int l, int t, int r, int b); + } + + public static class Toolbar extends ViewGroup { + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class Layouts { + ctor public Layouts(); + } + public static class Layouts.Toolbar extends test.pkg.Layouts.ViewGroup { + ctor public Layouts.Toolbar(); + } + public static class Layouts.View { + ctor public Layouts.View(); + method protected void onLayout(boolean, int, int, int, int); + } + public static abstract class Layouts.ViewGroup extends test.pkg.Layouts.View { + ctor public Layouts.ViewGroup(); + method protected abstract void onLayout(boolean, int, int, int, int); + } + } + """, + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Layouts { + public Layouts() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class Toolbar extends test.pkg.Layouts.ViewGroup { + public Toolbar() { throw new RuntimeException("Stub!"); } + protected void onLayout(boolean changed, int l, int t, int r, int b) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class View { + public View() { throw new RuntimeException("Stub!"); } + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract static class ViewGroup extends test.pkg.Layouts.View { + public ViewGroup() { throw new RuntimeException("Stub!"); } + protected abstract void onLayout(boolean changed, int l, int t, int r, int b); + } + } + """ + ) + } + + @Test + fun `Missing overridden method`() { + // Another special case where overridden methods were missing + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + import java.util.Collection; + import java.util.Set; + + @SuppressWarnings("all") + public class SpanTest { + public interface CharSequence { + } + public interface Spanned extends CharSequence { + public int nextSpanTransition(int start, int limit, Class type); + } + + public interface Spannable extends Spanned { + } + + public class SpannableString extends SpannableStringInternal implements CharSequence, Spannable { + } + + /* package */ abstract class SpannableStringInternal { + public int nextSpanTransition(int start, int limit, Class kind) { + return 0; + } + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class SpanTest { + public SpanTest() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface CharSequence { + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface Spannable extends test.pkg.SpanTest.Spanned { + } + @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"}) + public static interface Spanned extends test.pkg.SpanTest.CharSequence { + public int nextSpanTransition(int start, int limit, java.lang.Class type); + } + } + """ + ) + } + + @Test + fun `Skip type variables in casts`() { + // When generating casts in super constructor calls, use raw types + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + + @SuppressWarnings("all") + public class Properties { + public abstract class Property<T, V> { + public Property(Class<V> type, String name) { + } + public Property(Class<V> type, String name, String name2) { // force casts in super + } + } + + public abstract class IntProperty<T> extends Property<T, Integer> { + + public IntProperty(String name) { + super(Integer.class, name); + } + } + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Properties { + public Properties() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class IntProperty<T> extends test.pkg.Properties.Property<T,java.lang.Integer> { + public IntProperty(java.lang.String name) { super((java.lang.Class)null, (java.lang.String)null); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class Property<T, V> { + public Property(java.lang.Class<V> type, java.lang.String name) { throw new RuntimeException("Stub!"); } + public Property(java.lang.Class<V> type, java.lang.String name, java.lang.String name2) { throw new RuntimeException("Stub!"); } + } + } + """ + ) + } + + @Test + fun `Rewrite relative documentation links`() { + // When generating casts in super constructor calls, use raw types + checkStubs( + checkDoclava1 = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg1; + import java.io.IOException; + import test.pkg2.OtherClass; + + /** + * 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}. + * Here's an already fully qualified reference: {@link test.pkg2.OtherClass}. + * And here's one in the same package: {@link LocalClass}. + * + * @deprecated For some reason + * @see OtherClass + * @see OtherClass#bar(int, boolean) + */ + @SuppressWarnings("all") + public class SomeClass { + /** + * My method. + * @throws IOException when blah blah blah + * @throws {@link RuntimeException} when blah blah blah + */ + public void baz() throws IOException; + public boolean importance; + } + """ + ), + java( + """ + package test.pkg2; + + @SuppressWarnings("all") + public class OtherClass { + public int foo; + public void bar(int baz, boolean bar); + } + """ + ), + java( + """ + package test.pkg1; + + @SuppressWarnings("all") + public class LocalClass { + } + """ + ) + ), + warnings = "", + source = """ + package test.pkg1; + /** + * 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, + * boolean)}. + * And relative method reference {@link #baz()}. + * And relative field reference {@link #importance}. + * Here's an already fully qualified reference: {@link test.pkg2.OtherClass}. + * And here's one in the same package: {@link LocalClass}. + * + * @deprecated For some reason + * @see test.pkg2.OtherClass + * @see OtherClass#bar(int, boolean) + */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + @Deprecated public class SomeClass { + 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 + */ + public void baz() throws java.io.IOException { throw new RuntimeException("Stub!"); } + public boolean importance; + } + """ + ) + } + + // TODO: Add in some type variables in method signatures and constructors! + // TODO: Test what happens when a class extends a hidden extends a public in separate packages, + // and the hidden has a @hide constructor so the stub in the leaf class doesn't compile -- I should + // check for this and fail build. + + // TODO: Test -stubPackages +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/SystemServiceCheckTest.kt b/src/test/java/com/android/tools/metalava/SystemServiceCheckTest.kt new file mode 100644 index 0000000..8966c98 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/SystemServiceCheckTest.kt @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2017 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 org.junit.Test + +class SystemServiceCheckTest : DriverTest() { + @Test + fun `SystemService OK, loaded from signature file`() { + check( + warnings = "", // OK + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int myMethod1() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission + android:name="foo.bar.PERMISSION1" + android:label="@string/foo" + android:description="@string/foo" + android:protectionLevel="signature"/> + <permission + android:name="foo.bar.PERMISSION2" + android:protectionLevel="signature"/> + + </manifest> + """ + ) + } + + @Test + fun `SystemService OK, loaded from source`() { + check( + warnings = "", // OK + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @android.annotation.SystemService + public class MyTest2 { + @android.annotation.RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public void myMethod1() { + } + } + """ + ), + systemServiceSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission + android:name="foo.bar.PERMISSION1" + android:label="@string/foo" + android:description="@string/foo" + android:protectionLevel="signature"/> + <permission + android:name="foo.bar.PERMISSION2" + android:protectionLevel="signature"/> + + </manifest> + """ + ) + } + + @Test + fun `Check SystemService -- no permission annotation`() { + check( + warnings = "src/test/pkg/MyTest1.java:1: lint: Method 'myMethod2' must be protected with a system permission. [RequiresPermission:125]", + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + @android.annotation.SystemService + public class MyTest1 { + public int myMethod2() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest/> + """ + ) + } + + @Test + fun `Check SystemService -- can miss a permission with anyOf`() { + check( + warnings = "", + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int myMethod1() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission + android:name="foo.bar.PERMISSION1" + android:label="@string/foo" + android:description="@string/foo" + android:protectionLevel="signature"/> + </manifest> + """ + ) + } + + @Test + fun `Check SystemService -- at least one permission must be defined with anyOf`() { + check( + warnings = """ + src/test/pkg/MyTest2.java:2: lint: Method 'myMethod1' must be protected with a system permission. [RequiresPermission:125] + src/test/pkg/MyTest2.java:2: warning: None of the permissions foo.bar.PERMISSION1, foo.bar.PERMISSION2 are defined by manifest TESTROOT/manifest.xml. [RemovedField:10] + """, + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int myMethod1() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest/> + """ + ) + } + + @Test + fun `Check SystemService -- missing one permission with allOf`() { + check( + warnings = "src/test/pkg/MyTest2.java:2: warning: Permission 'foo.bar.PERMISSION2' is not defined by manifest TESTROOT/manifest.xml. [RemovedField:10]", + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(allOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int test() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission + android:name="foo.bar.PERMISSION1" + android:label="@string/foo" + android:description="@string/foo" + android:protectionLevel="signature"/> + </manifest> + """ + ) + } + + @Test + fun `Check SystemService -- must be system permission, not normal`() { + check( + warnings = "src/test/pkg/MyTest2.java:2: lint: Method 'test' must be protected with a system " + + "permission; it currently allows non-system callers holding [foo.bar.PERMISSION1, " + + "foo.bar.PERMISSION2] [RequiresPermission:125]", + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int test() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission + android:name="foo.bar.PERMISSION1" + android:label="@string/foo" + android:description="@string/foo" + android:protectionLevel="normal"/> + <permission + android:name="foo.bar.PERMISSION2" + android:protectionLevel="normal"/> + + </manifest> + """ + ) + } + + @Test + fun `Check SystemService -- missing manifest permissions`() { + check( + warnings = """ + src/test/pkg/MyTest2.java:2: lint: Method 'test' must be protected with a system permission. [RequiresPermission:125] + src/test/pkg/MyTest2.java:2: warning: Permission 'Manifest.permission.MY_PERMISSION' is not defined by manifest TESTROOT/manifest.xml. [RemovedField:10] + src/test/pkg/MyTest2.java:2: warning: Permission 'Manifest.permission.MY_PERMISSION2' is not defined by manifest TESTROOT/manifest.xml. [RemovedField:10] + """, + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(allOf={Manifest.permission.MY_PERMISSION,Manifest.permission.MY_PERMISSION2}) + public int test() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest/> + """ + ) + } + + @Test + fun `Invalid manifest`() { + check( + warnings = """ + TESTROOT/manifest.xml: error: Failed to parse TESTROOT/manifest.xml: The markup in the document preceding the root element must be well-formed. [ParseError:1] + src/test/pkg/MyTest2.java:2: lint: Method 'test' must be protected with a system permission. [RequiresPermission:125] + src/test/pkg/MyTest2.java:2: warning: None of the permissions foo.bar.PERMISSION1, foo.bar.PERMISSION2 are defined by manifest TESTROOT/manifest.xml. [RemovedField:10] + """, + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresPermission; + @SuppressWarnings("WeakerAccess") + @android.annotation.SystemService + public class MyTest2 { + @RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public int test() { } + } + """ + ), + systemServiceSource, + requiresPermissionSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + </error> + """ + ) + } + + @Test + fun `Warning suppressed via annotation`() { + check( + warnings = "", // OK (suppressed) + compatibilityMode = false, + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + @android.annotation.SystemService + public class MyTest1 { + @android.annotation.SuppressLint({"RemovedField","RequiresPermission"}) + @android.annotation.RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public void myMethod1() { + } + } + """ + ), + java( + """ + package test.pkg; + + @android.annotation.SystemService + public class MyTest2 { + // Old suppress syntax + @android.annotation.SuppressLint({"Doclava10","Doclava125"}) + @android.annotation.RequiresPermission(anyOf={"foo.bar.PERMISSION1","foo.bar.PERMISSION2"}) + public void myMethod1() { + } + } + """ + ), + systemServiceSource + ), + manifest = """<?xml version="1.0" encoding="UTF-8"?> + <manifest/> + """ + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt new file mode 100644 index 0000000..778408e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt @@ -0,0 +1,81 @@ +/* + * 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.apilevels + +import com.android.tools.metalava.DriverTest +import com.android.utils.XmlUtils +import com.google.common.truth.Truth +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import kotlin.text.Charsets.UTF_8 + +class ApiGeneratorTest : DriverTest() { + @Test + fun `Extract API levels`() { + val oldSdkJars = File("prebuilts/tools/common/api-versions") + if (!oldSdkJars.isDirectory) { + println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?") + return + } + + val platformJars = File("prebuilts/sdk") + if (!platformJars.isDirectory) { + println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars") + return + } + + val output = File.createTempFile("api-info", "xml") + output.deleteOnExit() + val outputPath = output.path + + check( + extraArguments = arrayOf( + "--generate-api-levels", + outputPath, + "--android-jar-pattern", + "${oldSdkJars.path}/android-%/android.jar", + "--android-jar-pattern", + "${platformJars.path}/%/android.jar" + ), + checkDoclava1 = false, + signatureSource = """ + package test.pkg { + public class MyTest { + ctor public MyTest(); + method public int clamp(int); + method public java.lang.Double convert(java.lang.Float); + field public java.lang.Number myNumber; + } + } + """ + ) + + assertTrue(output.isFile) + + val xml = output.readText(UTF_8) + Truth.assertThat(xml).contains("<class name=\"android/Manifest\$permission\" since=\"1\">") + Truth.assertThat(xml) + .contains("<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>") + + val document = XmlUtils.parseDocumentSilently(xml, false) + assertNotNull(document) + + } + +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/model/TextBackedAnnotationItemTest.kt b/src/test/java/com/android/tools/metalava/model/TextBackedAnnotationItemTest.kt new file mode 100644 index 0000000..cc4e121 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/model/TextBackedAnnotationItemTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 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 + +import com.android.tools.metalava.model.text.TextBackedAnnotationItem +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.function.Predicate + +class TextBackedAnnotationItemTest { + // Dummy for use in test where we don't need codebase functionality + private val dummyCodebase = object : DefaultCodebase() { + override fun supportsDocumentation(): Boolean = false + override var description: String = "" + override fun getPackages(): PackageList = unsupported() + override fun size(): Int = unsupported() + override fun findClass(className: String): ClassItem? = unsupported() + override fun findPackage(pkgName: String): PackageItem? = unsupported() + override fun trustedApi(): Boolean = false + override fun filter(filterEmit: Predicate<Item>, filterReference: Predicate<Item>): Codebase = unsupported() + override var supportsStagedNullability: Boolean = false + } + + @Test + fun testSimple() { + val annotation = TextBackedAnnotationItem( + dummyCodebase, + "@android.support.annotation.Nullable" + ) + assertEquals("@android.support.annotation.Nullable", annotation.toSource()) + assertEquals("android.support.annotation.Nullable", annotation.qualifiedName()) + assertTrue(annotation.attributes().isEmpty()) + } + + @Test + fun testIntRange() { + val annotation = TextBackedAnnotationItem( + dummyCodebase, + "@android.support.annotation.IntRange(from = 20, to = 40)" + ) + assertEquals("@android.support.annotation.IntRange(from = 20, to = 40)", annotation.toSource()) + assertEquals("android.support.annotation.IntRange", annotation.qualifiedName()) + assertEquals(2, annotation.attributes().size) + assertEquals("from", annotation.findAttribute("from")?.name) + assertEquals("20", annotation.findAttribute("from")?.value.toString()) + assertEquals("to", annotation.findAttribute("to")?.name) + assertEquals("40", annotation.findAttribute("to")?.value.toString()) + } + + @Test + fun testIntDef() { + val annotation = TextBackedAnnotationItem( + dummyCodebase, + "@android.support.annotation.IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT})" + ) + assertEquals( + "@android.support.annotation.IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT})", + annotation.toSource() + ) + assertEquals("android.support.annotation.IntDef", annotation.qualifiedName()) + assertEquals(1, annotation.attributes().size) + val attribute = annotation.findAttribute("value") + assertNotNull(attribute) + assertEquals("value", attribute?.name) + assertEquals( + "{STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}", + annotation.findAttribute("value")?.value.toString() + ) + + assertTrue(attribute?.value is AnnotationArrayAttributeValue) + if (attribute is AnnotationArrayAttributeValue) { + val list = attribute.values + assertEquals(3, list.size) + assertEquals("STYLE_NO_TITLE", list[1].toSource()) + } + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt b/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt new file mode 100644 index 0000000..29c919f --- /dev/null +++ b/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import com.android.tools.metalava.options +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TypeItemTest { + @Test + 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.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( + "java.util.List<@NonNull String>" + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt new file mode 100644 index 0000000..fe7e33d --- /dev/null +++ b/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt @@ -0,0 +1,54 @@ +/* + * 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.text + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TextTypeItemTest { + @Test + fun testTypeString() { + val full = + "@android.support.annotation.Nullable java.util.List<@android.support.annotation.Nullable java.lang.String>" + assertThat(TextTypeItem.toTypeString(full, false, false, false)).isEqualTo( + "java.util.List<java.lang.String>" + ) + assertThat(TextTypeItem.toTypeString(full, false, true, false)).isEqualTo( + "java.util.List<@android.support.annotation.Nullable java.lang.String>" + ) + assertThat(TextTypeItem.toTypeString(full, false, false, true)).isEqualTo( + "java.util.List" + ) + assertThat( + TextTypeItem.toTypeString( + full, + true, + true, + false + ) + ).isEqualTo("@android.support.annotation.Nullable java.util.List<@android.support.annotation.Nullable java.lang.String>") + assertThat( + TextTypeItem.toTypeString( + full, + true, + true, + true + ) + ).isEqualTo("@android.support.annotation.Nullable java.util.List") + assertThat(TextTypeItem.toTypeString("int", false, false, false)).isEqualTo("int") + } +} \ No newline at end of file diff --git a/stub-annotations/build.gradle b/stub-annotations/build.gradle new file mode 100644 index 0000000..9a6cedc --- /dev/null +++ b/stub-annotations/build.gradle @@ -0,0 +1,4 @@ +apply plugin: 'java-library' + +sourceCompatibility = "1.8" +targetCompatibility = "1.8" diff --git a/stub-annotations/src/main/java/android/support/annotation/Migrate.java b/stub-annotations/src/main/java/android/support/annotation/Migrate.java new file mode 100644 index 0000000..233ce52 --- /dev/null +++ b/stub-annotations/src/main/java/android/support/annotation/Migrate.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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 android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface Migrate { +} diff --git a/stub-annotations/src/main/java/android/support/annotation/NewlyNonNull.java b/stub-annotations/src/main/java/android/support/annotation/NewlyNonNull.java new file mode 100644 index 0000000..83f4e5a --- /dev/null +++ b/stub-annotations/src/main/java/android/support/annotation/NewlyNonNull.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 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 android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +@Migrate +public @interface NewlyNonNull { +} diff --git a/stub-annotations/src/main/java/android/support/annotation/NewlyNullable.java b/stub-annotations/src/main/java/android/support/annotation/NewlyNullable.java new file mode 100644 index 0000000..e11fe4f --- /dev/null +++ b/stub-annotations/src/main/java/android/support/annotation/NewlyNullable.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +@Migrate +public @interface NewlyNullable { +} diff --git a/stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java b/stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java new file mode 100644 index 0000000..d2309fc --- /dev/null +++ b/stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +@Migrate +public @interface RecentlyNonNull { +} diff --git a/stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java b/stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java new file mode 100644 index 0000000..d24bad0 --- /dev/null +++ b/stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +@Migrate +public @interface RecentlyNullable { +} -- GitLab