From 52ffe48fd214c9b6e39ac6d4f30cff9afbe9fa25 Mon Sep 17 00:00:00 2001 From: Aurimas Liutikas <aurimas@google.com> Date: Fri, 26 Jan 2018 16:15:34 -0800 Subject: [PATCH] Initial checkin of metalava. This includes the following internal changes: ag/3072760 ag/3466329 ag/3474287 Test: ./gradlew assemble Change-Id: Iecc7996ec8f2cbb453d84e1c63f01ca046464da3 --- .gitignore | 6 + README.md | 439 +++ build.gradle | 53 + 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 | 407 +++ .../tools/metalava/AnnotationsMerger.kt | 724 +++++ .../com/android/tools/metalava/ApiAnalyzer.kt | 1050 +++++++ .../tools/metalava/ComparisonVisitor.kt | 291 ++ .../android/tools/metalava/Compatibility.kt | 162 + .../tools/metalava/CompatibilityCheck.kt | 148 + .../com/android/tools/metalava/DocAnalyzer.kt | 513 ++++ .../java/com/android/tools/metalava/Driver.kt | 589 ++++ .../tools/metalava/ExtractAnnotations.kt | 51 + .../tools/metalava/NullnessMigration.kt | 163 + .../com/android/tools/metalava/Options.kt | 1076 +++++++ .../android/tools/metalava/ProguardWriter.kt | 134 + .../com/android/tools/metalava/Reporter.kt | 272 ++ .../android/tools/metalava/SignatureWriter.kt | 294 ++ .../com/android/tools/metalava/StubWriter.kt | 565 ++++ .../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 | 861 ++++++ .../tools/metalava/doclava1/ApiInfo.kt | 263 ++ .../metalava/doclava1/ApiParseException.java | 49 + .../tools/metalava/doclava1/ApiPredicate.kt | 100 + .../metalava/doclava1/ElidingPredicate.kt | 31 + .../tools/metalava/doclava1/Errors.java | 236 ++ .../metalava/doclava1/SourcePositionInfo.java | 66 + .../tools/metalava/model/AnnotationItem.kt | 412 +++ .../android/tools/metalava/model/ClassItem.kt | 652 ++++ .../android/tools/metalava/model/Codebase.kt | 178 ++ .../tools/metalava/model/CompilationUnit.kt | 35 + .../tools/metalava/model/ConstructorItem.kt | 27 + .../android/tools/metalava/model/FieldItem.kt | 303 ++ .../com/android/tools/metalava/model/Item.kt | 223 ++ .../tools/metalava/model/MemberItem.kt | 27 + .../tools/metalava/model/MethodItem.kt | 389 +++ .../tools/metalava/model/ModifierList.kt | 290 ++ .../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 | 134 + .../tools/metalava/model/psi/ClassType.kt | 37 + .../tools/metalava/model/psi/Javadoc.kt | 219 ++ .../metalava/model/psi/PsiAnnotationItem.kt | 380 +++ .../metalava/model/psi/PsiBasedCodebase.kt | 949 ++++++ .../tools/metalava/model/psi/PsiClassItem.kt | 769 +++++ .../metalava/model/psi/PsiConstructorItem.kt | 249 ++ .../tools/metalava/model/psi/PsiFieldItem.kt | 137 + .../tools/metalava/model/psi/PsiItem.kt | 302 ++ .../tools/metalava/model/psi/PsiMethodItem.kt | 294 ++ .../metalava/model/psi/PsiModifierItem.kt | 297 ++ .../metalava/model/psi/PsiPackageItem.kt | 163 + .../metalava/model/psi/PsiParameterItem.kt | 123 + .../tools/metalava/model/psi/PsiTypeItem.kt | 510 ++++ .../model/text/TextBackedAnnotationItem.kt | 59 + .../metalava/model/text/TextClassItem.kt | 244 ++ .../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 | 76 + .../metalava/model/visitors/ApiVisitor.kt | 65 + .../metalava/model/visitors/ItemVisitor.kt | 77 + .../model/visitors/PredicateVisitor.kt | 40 + .../metalava/model/visitors/TypeVisitor.kt | 26 + .../model/visitors/VisibleItemVisitor.kt | 39 + .../metalava/AnnotationStatisticsTest.kt | 162 + .../tools/metalava/AnnotationsMergerTest.kt | 127 + .../com/android/tools/metalava/ApiFileTest.kt | 1975 ++++++++++++ .../android/tools/metalava/ApiFromTextTest.kt | 280 ++ .../tools/metalava/CompatibilityCheckTest.kt | 343 +++ .../android/tools/metalava/DocAnalyzerTest.kt | 1028 +++++++ .../com/android/tools/metalava/DriverTest.kt | 967 ++++++ .../tools/metalava/ExtractAnnotationsTest.kt | 430 +++ .../android/tools/metalava/KeepFileTest.kt | 84 + .../tools/metalava/NullnessMigrationTest.kt | 253 ++ .../com/android/tools/metalava/OptionsTest.kt | 214 ++ .../tools/metalava/ShowAnnotationTest.kt | 56 + .../com/android/tools/metalava/StubsTest.kt | 2667 +++++++++++++++++ .../tools/metalava/SystemServiceCheckTest.kt | 365 +++ .../metalava/apilevels/ApiGeneratorTest.kt | 80 + .../model/TextBackedAnnotationItemTest.kt | 93 + 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 + 110 files changed, 28327 insertions(+) create mode 100644 .gitignore 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/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/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/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/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 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..0a612ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +build/ +metalava.iml +.gradle +.DS_Store +out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..40c5fd1 --- /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 `build/install/metalava`. + +To run metalava: + + $ ./build/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..983ae5f --- /dev/null +++ b/build.gradle @@ -0,0 +1,53 @@ +buildscript { + ext.gradle_version = '3.1.0-alpha08' + ext.studio_version = '26.1.0-alpha08' + ext.kotlin_version = '1.2.20' + 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' + +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() + maven { url '/Users/tnorbye/dev/studio/dev/out/repo' } +} + +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" +} + +defaultTasks 'installDist' + 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..6c7c1ee --- /dev/null +++ b/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt @@ -0,0 +1,407 @@ +/* + * 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.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) { + /** 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 + return cls.findField(node.name) + } + + private fun findClass(owner: String): ClassItem? { + val className = owner.replace('/', '.').replace('$', '.') + return api.findClass(className) + } + + 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 + return cls.findMethod(methodName, parameters) + } +} \ 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..04b4094 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt @@ -0,0 +1,724 @@ +/* + * 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.* +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..9e6bd0c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt @@ -0,0 +1,1050 @@ +/* + * 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.* +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(filterEmit: 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() + + 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 filterEmit here to not waste time in hidden packages + .filter { filterEmit.test(it) } + .forEach { addConstructors(it, filterEmit, 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, filterEmit: 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.superClass() + superClass?.let { it -> addConstructors(it, filterEmit, false) } + cls.tag = true + + // Use + if (superClass != null) { + val superDefaultConstructor = superClass.defaultConstructor + if (superDefaultConstructor != null) { + for (constructor in cls.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) { + val constructors = cls.constructors() + for (constructor in constructors) { + if (constructor.parameters().isEmpty() && constructor.isPublic) { + cls.defaultConstructor = constructor + return + } + } + + // Try to pick the constructor with the fewest throwables, and among those + // the ones with the fewest parameters + if (!constructors.isEmpty()) { + // Try to pick the constructor, sorting first by "matches filter", then by + // fewest throwables, then fewest parameters, then based on order in list + val first = constructors.first() + cls.defaultConstructor = constructors.foldRight(first) { current, next -> + pickBest(current, next, filterEmit) + } + return + } else { + // No constructors, yet somebody extends this: 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 + } + } + } + + if (cls.hasPrivateConstructor) { + // TODO: There was a private constructor: we have to make sure we insert one, + // such that the class doesn't suddenly get an implicit constructor! + } + } + + // 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, + filterEmit: Predicate<Item> + ): ConstructorItem { + val currentMatchesFilter = filterEmit.test(current) + val nextMatchesFilter = filterEmit.test(next) + if (currentMatchesFilter != nextMatchesFilter) { + return if (currentMatchesFilter) { + 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 { !filterEmit.test(it) && !it.isJavaLangObject() } + if (hiddenSuperClasses.none()) { // not missing any implementation methods + return + } + + generateInheritedStubsFrom(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 generateInheritedStubsFrom( + 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, "0:0", 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>, why: String, + 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, "2:" + cl.qualifiedName(), + stubImportPackages + ) + } + for (cls in fieldType.typeArgumentClasses()) { + cantStripThis( + cls, notStrippable, "3:" + cl.qualifiedName(), + stubImportPackages + ) + } + } + } + // cant strip any of the type's generics + for (cls in cl.typeArgumentClasses()) { + cantStripThis( + cls, notStrippable, "4:" + cl.qualifiedName(), + 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, "5:" + cl.qualifiedName(), + 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, "6:" + cl.superClass()?.simpleName() + cl.qualifiedName(), + 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, + "8:${method.containingClass().qualifiedName()}:${method.name()}", + stubImportPackages + ) + } + for (parameter in method.parameters()) { + for (parameterTypeClass in parameter.type().typeArgumentClasses()) { + cantStripThis( + parameterTypeClass, notStrippable, "9:" + + method.containingClass().qualifiedName() + ":" + method.name(), + 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, + "10:${method.containingClass().qualifiedName()}:${method.name()}", + stubImportPackages + ) + } + } + } + } + for (thrown in method.throwsTypes()) { + cantStripThis( + thrown, notStrippable, ("11:" + method.containingClass().qualifiedName() + + ":" + method.name()), stubImportPackages + ) + } + val returnType = method.returnType() + if (returnType != null && !returnType.primitive) { + val returnTypeClass = returnType.asClass() + if (returnTypeClass != null) { + cantStripThis( + returnTypeClass, notStrippable, + "12:${method.containingClass().qualifiedName()}:${method.name()}", + stubImportPackages + ) + for (tyItem in returnType.typeArgumentClasses()) { + cantStripThis( + tyItem, notStrippable, + "13:${method.containingClass().qualifiedName()}:${method.name()}", + 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..7957219 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/ComparisonVisitor.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 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.ItemVisitor +import com.intellij.util.containers.Stack +import java.util.* + +/** + * 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) { + // Algorithm: build up two trees (by nesting level); then visit the + // two trees + val oldTree = createTree(old) + val newTree = createTree(new) + 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): List<ItemTree> { + // TODO: Make sure the items are sorted! + val stack = Stack<ItemTree>() + val root = ItemTree(null) + stack.push(root) + codebase.accept(object : ItemVisitor(nestInnerClasses = true, skipEmptyPackages = true) { + 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..c16f489 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt @@ -0,0 +1,148 @@ +/* + * 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) { + // Adding a package isn't technically a compatibility error, but doclava1 flags it as a warning + // so we will too + 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 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/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt new file mode 100644 index 0000000..ac3f432 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -0,0 +1,513 @@ +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.* +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") { + 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..c593ddc --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -0,0 +1,589 @@ +/* + * 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.Errors +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.psi.PsiBasedCodebase +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.regex.Pattern + +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) { + 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() + 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) + } finally { + previous.supportsStagedNullability = prev + } + } +} + +private fun checkCompatibility(codebase: Codebase, previous: Codebase) { + if (options.checkCompatibility) { + previous.compareWith(CompatibilityCheck(), 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.showAnnotations.isEmpty() + val filterEmit = ApiPredicate(codebase, ignoreShown = ignoreShown, ignoreRemoved = false) + + analyzer.addConstructors(filterEmit) + + if (options.stubsDir != null) { + 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) +} + +private fun filterCodebase(codebase: PsiBasedCodebase): Codebase { + val ignoreShown = options.showAnnotations.isEmpty() + + // We ignore removals when limiting the API + val remove = false + val filterEmit = ApiPredicate(codebase, ignoreShown = ignoreShown, ignoreRemoved = remove, allowFromJar = false) + val filterReference = ApiPredicate(codebase, ignoreShown = true, ignoreRemoved = remove, allowFromJar = true) + + // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary + // TODO: Only do this for stub generation? + progress("\nInsert missing stubs methods: ") + ApiAnalyzer(codebase).generateInheritedStubs(filterEmit, filterReference) + + return codebase.filter(filterEmit, filterReference) +} + +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 -> createApiSignatureFile(apiFile, codebase) } + options.removedApiFile?.let { apiFile -> createRemovedSignatureFile(apiFile, codebase) } + options.proguard?.let { proguard -> createProguardFile(proguard, codebase) } + options.stubsDir?.let { createStubFiles(it, codebase) } + 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, stubDir, ignoreShown = true, generateAnnotations = options.generateAnnotations) + 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) + } + + compatibility = prevCompatibility + + progress("\n$PROGRAM_NAME wrote stubs directory $stubDir in $localTimer") +} + +private fun createApiSignatureFile(apiFile: File, codebase: Codebase) { + progress("\nWriting API signature file: ") + createSignatureFile(codebase, apiFile, false) +} + +private fun createRemovedSignatureFile(apiFile: File, codebase: Codebase) { + progress("\nWriting removed API signature file: ") + + // When generating removed signature files, we operate on the original sources that contain removed elements too + val original = codebase.original!! + + createSignatureFile(original, apiFile, true) +} + +private fun progress(message: String) { + if (options.verbose) { + options.stdout.print(message) + options.stdout.flush() + } +} + +private fun createProguardFile(apiFile: File, codebase: Codebase) { + val localTimer = Stopwatch.createStarted() + try { + val writer = PrintWriter(Files.asCharSink(apiFile, Charsets.UTF_8).openBufferedStream()) + writer.use { printWriter -> + val apiWriter = ProguardWriter(codebase, 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 API file $apiFile in $localTimer") + } +} + +private fun createSignatureFile(codebase: Codebase, apiFile: File, showRemovedApi: Boolean) { + val localTimer = Stopwatch.createStarted() + try { + val writer = PrintWriter(Files.asCharSink(apiFile, Charsets.UTF_8).openBufferedStream()) + writer.use { printWriter -> + val ignoreShown = options.showAnnotations.isEmpty() + val apiWriter = SignatureWriter(codebase, printWriter, ignoreShown, showRemovedApi) + 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 API file $apiFile in $localTimer") + } +} + +/** 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..c73c0d5 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/NullnessMigration.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.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. + */ +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..e925b39 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -0,0 +1,1076 @@ +/* + * 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 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_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_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 [stubPackages] */ + private val mutableStubPackages: MutableSet<String> = mutableSetOf() + /** 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 + + /** Packages to include (if empty, include all) */ + var stubPackages: Set<String> = mutableStubPackages + + /** 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 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 = 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_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)) + + "--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) + mutableStubPackages += 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_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) + } + + 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", + + "", "\nExtracting Signature Files:", + // TODO: Document --show-annotation! + ARG_API + " <file>", "Generate a signature descriptor file", + 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/ProguardWriter.kt b/src/main/java/com/android/tools/metalava/ProguardWriter.kt new file mode 100644 index 0000000..258a74c --- /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.Codebase +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.ParameterItem +import com.android.tools.metalava.model.TypeItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import java.io.PrintWriter + +class ProguardWriter( + codebase: Codebase, + private val writer: PrintWriter, + ignoreShown: Boolean = options.showAnnotations.isEmpty() +) : ApiVisitor( + codebase = codebase, + visitConstructorsAsMethods = false, + nestInnerClasses = false, + ignoreShown = ignoreShown, + remove = false, + elide = true +) { + + 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..43e5009 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Reporter.kt @@ -0,0 +1,272 @@ +/* + * 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) { + + 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 + } + + 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() + } +} \ 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..ce43acb --- /dev/null +++ b/src/main/java/com/android/tools/metalava/SignatureWriter.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 + +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.TypeItem +import com.android.tools.metalava.model.visitors.ApiVisitor +import java.io.PrintWriter + +class SignatureWriter( + codebase: Codebase, + private val writer: PrintWriter, + ignoreShown: Boolean = false, + remove: Boolean = false, + private val prefiltered: Boolean = !remove +) : ApiVisitor( + codebase = codebase, + visitConstructorsAsMethods = false, + nestInnerClasses = false, + ignoreShown = ignoreShown, + remove = remove, + elide = true, + methodComparator = MethodItem.comparator, + fieldComparator = FieldItem.comparator +) { + + 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 = + if (!compatibility.omitTypeParametersInInterfaces) { + superClass.toString() + } else { + superClass.toErasedTypeString() + } + 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(" ") + if (!compatibility.omitTypeParametersInInterfaces) { + writer.print(item.toString()) + } else { + writer.print(item.toErasedTypeString()) + } + } + } + } + + 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 + + val typeString = type.toString() + // Strip java.lang. prefix? + if (options.omitCommonPackages && + typeString.startsWith("java.lang.") && typeString.indexOf('.', "java.lang.".length) == -1) { + writer.print(typeString.substring("java.lang.".length)) + } else { + 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..62c6ee8 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/StubWriter.kt @@ -0,0 +1,565 @@ +/* + * 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.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, + ignoreShown: Boolean = false, + private val generateAnnotations: Boolean = false, + private val prefiltered: Boolean = true +) : ApiVisitor( + codebase = codebase, + visitConstructorsAsMethods = false, + nestInnerClasses = true, + ignoreShown = ignoreShown, + remove = false, + elide = false, + 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 +) { + + private val sourceList = StringBuilder(20000) + + /** 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.toString() + 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.toFullyQualifiedString()) + } + } 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) { + val containingClass = constructor.containingClass() + // public MarginLayoutParams(android.content.Context c, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); } + + // Attempt to pick the real super method instead of just the first one, + // if there is a match + constructor.superConstructor?.let { superMethod -> + if (filterEmit.test(superMethod)) { + writeConstructor(constructor, superMethod) + return + } + } + + // Attempt to match it via indirect hidden intermediaries + val publicSuperClass = containingClass.publicSuperClass() + if (publicSuperClass != null) { + val publicSuperClassConstructors = publicSuperClass.constructors() + + // Try to pick a constructor that is at least public + for (superConstructor in publicSuperClassConstructors) { + if (filterEmit.test(superConstructor)) { + writeConstructor(constructor, superConstructor) + return + } + } + } + + writeConstructor(constructor, publicSuperClass?.constructors()?.firstOrNull()) + } + + 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(" { ") + + if (superConstructor != null && !filterReference.test(superConstructor)) { + // Using real signature from public constructor but the super constructor is not + // available: that means it might have an unexpected set of exceptions that we don't + // catch here, so prepare for the worst + writeThrowStub() + writer.println(" }") + return + } + + + 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 + if (clsDefaultConstructor != null && !cls.constructors().contains(clsDefaultConstructor)) { + visitConstructor(clsDefaultConstructor) + return + } + + if (cls.isClass()) { + val constructors = cls.constructors().asSequence().filter { filterEmit.test(it) } + if (constructors.none() && cls.hasPrivateConstructor) { + val superConstructor = cls.filteredSuperclass(filterEmit)?.defaultConstructor + if (superConstructor != null) { + generateTypeParameterList(typeList = superConstructor.typeParameterList(), addSpace = true) + } + + // Not writing modifiers: leave package private since this wasn't a public constructor + writer.print(cls.simpleName()) + + if (superConstructor != null) { + // What if the super constructor isn't public? In that case it may reference hidden + // types that we don't include stubs for! + generateParameterList(superConstructor) + generateThrowsList(superConstructor) + writer.print(" { ") + + // TODO: I need to be careful here: what if the cast types are hidden? + writeConstructorBody(null, superConstructor) + } else { + writer.print("() { ") + writeThrowStub() + } + //writeConstructorBody(superClass.constructors().first()) + writer.println(" }") + } + } + } + + 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().toString() == "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?.toString()) + + 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().toString()) + 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().toString()) + 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..9f74d98 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java @@ -0,0 +1,861 @@ +/* + * 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 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(); + } + 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, + false/*isPrivate*/, 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 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(); + } + 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, false/*isPrivate*/, 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 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(); + } + 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, false/*isPrivate*/, 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 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(); + } + 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, false/*isPrivate*/, 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..0b680b3 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt @@ -0,0 +1,263 @@ +/* + * 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.* +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) { + CodebaseComparator().compare(visitor, this, other) + } + + 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..ecf1858 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt @@ -0,0 +1,100 @@ +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 = false, + /** + * 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..d681fb7 --- /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(member: Item): Boolean { + // This member should be included, but if it's an exact duplicate + // override then we can elide it. + return if (member is MethodItem) { + val different = member.findPredicateSuperMethod(Predicate { test -> + // We're looking for included and perfect signature + wrapped.test(test) && + test is MethodItem && + MethodItem.sameSignature(member, test, false) + }) + different == 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/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..029b9e7 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt @@ -0,0 +1,412 @@ +/* + * 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.RECENTLY_NONNULL +import com.android.tools.metalava.RECENTLY_NULLABLE +import com.android.tools.metalava.options + +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?): 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.Widget" -> { + // Remove, unless specifically included in --showAnnotations + return if (options.showAnnotations.contains(qualifiedName)) { + qualifiedName + } else { + null + } + } + + // 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('.'))}" + qualifiedName.startsWith("android.annotation.") -> "android.support.annotation." + qualifiedName.substring("android.annotation.".length) + // Other third party nullness annotations? + isNullableAnnotation(qualifiedName) -> "android.support.annotation.Nullable" + isNonNullAnnotation(qualifiedName) -> "android.support.annotation.NonNull" + else -> qualifiedName + } + } + } + } + + /** + * Given a "full" annotation name, shortens it by removing redundant package names. + * This is intended to be used by the [com.android.tools.metalava.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 { + 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..95a84ba --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt @@ -0,0 +1,652 @@ +/* + * 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.doclava1.ElidingPredicate +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.* +import java.util.function.Predicate +import kotlin.Comparator + +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('.', '$') + } + + /** 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 + } + + /** + * 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.showAnnotations.isEmpty()) { + 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 + val filterReference = visitor.filterReference + + constructors = cls.constructors().asSequence().filter { filterEmit.test(it) } + .sortedWith(MethodItem.comparator) + val elidingPredicate = ElidingPredicate(filterReference) + methods = cls.filteredMethods(filterEmit).asSequence() + .filter { !visitor.elide || elidingPredicate.test(it) } + .sortedWith(MethodItem.comparator) + + if (cls.isEnum()) { + fields = cls.filteredFields(filterEmit).asSequence() + .filter({ !it.isEnumConstant() }) + .sortedWith(FieldItem.comparator) + enums = cls.filteredFields(filterEmit).asSequence() + .filter({ it.isEnumConstant() }) + .sortedWith(FieldItem.comparator) + } else { + fields = cls.filteredFields(filterEmit).asSequence() + .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..ff391e7 --- /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) { + CodebaseComparator().compare(visitor, other, this) + } + + /** + * 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..3b37b1d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ConstructorItem.kt @@ -0,0 +1,27 @@ +/* + * 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 + + /** + * 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..8f76a00 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/FieldItem.kt @@ -0,0 +1,303 @@ +/* + * 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..0c5d504 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/MemberItem.kt @@ -0,0 +1,27 @@ +/* + * 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 + + /** 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..7979c0b --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt @@ -0,0 +1,389 @@ +/* + * 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.* +import java.util.function.Predicate +import kotlin.Comparator + +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) + if (visitor.visitConstructorsAsMethods) { + visitor.visitMethod(this) + } + } else { + visitor.visitMethod(this) + } + + for (parameter in parameters()) { + parameter.accept(visitor) + } + + if (isConstructor()) { + if (visitor.visitConstructorsAsMethods) { + visitor.afterVisitConstructor(this as ConstructorItem) + } + 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().toString().compareTo(p2[i].type().toString(), 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().toString()) + } + + 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..9fac5d8 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt @@ -0,0 +1,290 @@ +/* + * 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..b6a0370 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/TypeItem.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.model + +import com.android.tools.metalava.compatibility + +/** Represents a type */ +interface TypeItem { + override fun toString(): String + + fun toErasedTypeString(): String + + fun toFullyQualifiedString(): String + + fun asClass(): ClassItem? + + fun toSimpleType(): String { + return toString().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(toString(), replacementMap) + } + + fun isJavaLangObject(): Boolean { + return toString() == "java.lang.Object" + } + + fun defaultValue(): Any? { + return when (toString()) { + "boolean" -> false + "byte" -> 0.toByte() + "char" -> '\u0000' + "short" -> 0.toShort() + "int" -> 0 + "long" -> 0L + "float" -> 0f + "double" -> 0.0 + else -> null + } + } + + fun defaultValueString(): String = defaultValue()?.toString() ?: "null" + + companion object { + fun formatType(type: String?): String { + if (type == null) { + return "" + } + if (compatibility.spacesAfterCommas && type.indexOf(',') != -1) { + // The compat files have spaces after commas where we normally don't + return type.replace(",", ", ").replace(", ", ", ") + } + + return cleanupGenerics(type) + } + + 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.toString().compareTo(type2.toString()) + } + } + + 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 + } + } + } + + fun hasTypeArguments(): Boolean = toString().contains("<") + + fun isTypeParameter(): Boolean = toString().length == 1 // heuristic; accurate implementation in PSI subclass +} \ 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..dd69da4 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt @@ -0,0 +1,380 @@ +/* + * 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..52d905d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt @@ -0,0 +1,949 @@ +/* + * 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.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.* +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 + } + + 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) + + 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.filteredMethods(filterEmit).asSequence() + 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..a854ef4 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt @@ -0,0 +1,769 @@ +/* + * 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() + .filterNot { it.isPrivate() } + .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() + .filterNot { + // Skip private members, unless they're annotations + // (we want to be able to resolve these) + it.isPrivate() && !it.isAnnotationType + } + .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..06a2dfd --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt @@ -0,0 +1,302 @@ +/* + * 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 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 = 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 + 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()) { + val exceptionName = dataElements[0].text + val exceptionReference = codebase.createReferenceFromText(exceptionName, psi()) + resolved = exceptionReference.resolve() + referenceText = exceptionName + } else { + return text + } + } + + if (resolved != null && referenceText != null) { + + 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 + } + val valueElement = tag.valueElement + 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 + val valueElement = tag.valueElement + 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..0737fc7 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt @@ -0,0 +1,297 @@ +/* + * 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..96ac070 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt @@ -0,0 +1,510 @@ +/* + * 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.Item +import com.android.tools.metalava.model.TypeItem +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 toErasedString: String? = null + private var asClass: ClassItem? = null + + override fun toString(): String { + if (toString == null) { + toString = TypeItem.formatType(psiType.canonicalText) + } + return toString!! + } + + override fun toErasedTypeString(): String { + if (toErasedString == null) { + toErasedString = TypeConversionUtil.erasure(psiType).canonicalText + } + return toErasedString!! + } + + override fun toFullyQualifiedString(): String { + return toString() + } + + override fun isTypeParameter(): Boolean { + return asClass()?.psi() is PsiTypeParameter + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + return when (other) { + is TypeItem -> toString().replace(" ", "") == other.toString().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 { + 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..15a21cf --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt @@ -0,0 +1,244 @@ +/* + * 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..2a1b607 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt @@ -0,0 +1,76 @@ +/* + * 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 { + val s = toString() + val index = s.indexOf('<') + if (index != -1) { + return s.substring(0, index) + } + return s + } + + override fun toFullyQualifiedString(): String = type + + 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 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..2665906 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/ApiVisitor.kt @@ -0,0 +1,65 @@ +/* + * 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.MethodItem + +open class ApiVisitor( + 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, + + /** Whether to elide methods that are identical in signature to inherited methods */ + val elide: Boolean = false, + + /** 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 +) : ItemVisitor(visitConstructorsAsMethods, nestInnerClasses) { + + val filterEmit: ApiPredicate = ApiPredicate(codebase, ignoreShown = ignoreShown, matchRemoved = remove) + val filterReference: ApiPredicate = 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 +} \ No newline at end of file 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..c21d23d --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/visitors/ItemVisitor.kt @@ -0,0 +1,77 @@ +/* + * 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..4d8873f --- /dev/null +++ b/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.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 + +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 7 methods were annotated (57%) + 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..1e9459e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt @@ -0,0 +1,1975 @@ +/* + * 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 { + protected 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(); + } + } + """ + ) + } + + @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 `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; + + @SuppressWarnings("UnnecessaryInterfaceModifier") + public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { + public void close(); + } + """ + ), + java( + """ + package android.util; + @SuppressWarnings("WeakerAccess") + public interface AttributeSet { + } + """ + ), + java( + """ + package java.lang; + 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 java.lang.AutoCloseable org.xmlpull.v1.XmlPullParser { + method public abstract void close(); + } + } + package android.util { + public abstract interface AttributeSet { + } + } + package java.lang { + 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..3fe7b01 --- /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>> implements java.lang.AutoCloseable { + } + } + 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..d18657a --- /dev/null +++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt @@ -0,0 +1,343 @@ +/* + * 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); + } + } + """ + ) + } + + // 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..4f1c94d --- /dev/null +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -0,0 +1,1028 @@ +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() { + @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..b64af1a --- /dev/null +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -0,0 +1,967 @@ +/* + * 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.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, + /** 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(), + /** 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 + ) { + + 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() + } + + 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 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, + *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, + *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 (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", apiFile, apiFile, sourceList, sourcePath, packages, androidJar, + trim, stripBlankLines, showAnnotationArguments, importedPackages + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && + exactApi != null && exactApiFile != null + ) { + exactApiFile.delete() + checkSignaturesWithDoclava1( + exactApi, "-exactApi", exactApiFile, exactApiFile, sourceList, sourcePath, + packages, androidJar, trim, stripBlankLines, showAnnotationArguments, importedPackages + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null + && removedApi != null && removedApiFile != null + ) { + removedApiFile.delete() + checkSignaturesWithDoclava1( + removedApi, "-removedApi", removedApiFile, removedApiFile, sourceList, + sourcePath, packages, androidJar, trim, stripBlankLines, showAnnotationArguments, importedPackages + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null && stubsDir != null) { + stubsDir.deleteRecursively() + val firstFile = File(stubsDir, sourceFiles[0].targetPath.substring("src/".length)) + checkSignaturesWithDoclava1( + stubs[0], "-stubs", stubsDir, firstFile, sourceList, sourcePath, packages, + androidJar, trim, stripBlankLines, showAnnotationArguments, importedPackages + ) + } + + if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && proguard != null && proguardFile != null) { + proguardFile.delete() + checkSignaturesWithDoclava1( + proguard, "-proguard", proguardFile, proguardFile, sourceList, + sourcePath, packages, androidJar, trim, stripBlankLines, showAnnotationArguments, importedPackages + ) + } + } + + 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> + ) { + // 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 showAnnotation: Array<String> = if (showAnnotationArgs.isNotEmpty()) { + showAnnotationArgs.map { if (it == "--show-annotation") "-showAnnotation" else it }.toTypedArray() + } else { + emptyArray() + } + + val docLava1 = File("testlibs/doclava1.jar") + if (!docLava1.isFile) { + 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, + + *showAnnotation, + *hidePackageArgs.toTypedArray(), + + // -api, or // -stub, etc + doclavaArg, + output.path + ) + + 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}) +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}) +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}) +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}) +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..0553b9e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt @@ -0,0 +1,430 @@ +/* + * 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; +} +""" + ) + + @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; +} +""" + ) + + @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(); + } +}""" + ) + + @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 { +}""" + ) + + @SuppressWarnings("all") // sample code + private val packageTest = java( + """ +@IntRange(from = 20) +package test.pkg; + +import android.support.annotation.IntRange;""" + ) + + @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) { + } + } +}""" + ) + + @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"; + } +} +""" + ) + + 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..4d3e7e9 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt @@ -0,0 +1,253 @@ +/* + * 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(); + } + } + """ + ) + } +} \ 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..b4a748b --- /dev/null +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -0,0 +1,214 @@ +/* + * 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 + +Extracting Signature Files: +--api <file> Generate a signature descriptor file +--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 no arguments`() { + val args = emptyList<String>() + + 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() + ) + } + + @Test + fun `Test invalid arguments`() { + val args = listOf("--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("--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/ShowAnnotationTest.kt b/src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt new file mode 100644 index 0000000..e71e158 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/ShowAnnotationTest.kt @@ -0,0 +1,56 @@ +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() { } + + } + """ + ), + 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(); + } + } + """ + ) + } +} \ 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..62a7240 --- /dev/null +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -0,0 +1,2667 @@ +/* + * 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 `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, i, l, b, sh); + } + } + + public class Child2 extends Parent { + Child2(String s) { + super(s, i, l, b, sh); + } + } + + public class Child3 extends Child2 { + private Child3(String s) { + super("something"); + } + } + } + """ + ) + ), + 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() { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Child3 extends test.pkg.Constructors.Child2 { + Child3() { 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 `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 { + MyClass1(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 MyClass2() { 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() { throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MySubClass1 extends test.pkg.MyClass1 { + MySubClass1() { throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass2 { + MyClass2() { throw new RuntimeException("Stub!"); } + } + """, + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MySubClass2 extends test.pkg.MyClass2 { + public MySubClass2() { 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 implements java.io.Serializable { + ctor public PickConstructors.FileNotFoundException(); + } + public static class PickConstructors.IOException extends java.lang.Exception implements java.io.Serializable { + 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 implements java.io.Serializable { + public FileNotFoundException() { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static class IOException extends java.lang.Exception implements java.io.Serializable { + 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 This 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) { this(); 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 reference {@link #baz()}. + * 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 + */ + public void baz() throws IOException; + } + """ + ), + 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 reference {@link #baz()}. + * 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 + */ + public void baz() throws java.io.IOException { throw new RuntimeException("Stub!"); } + } + """ + ) + } + + // 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. +} \ 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..07c676c --- /dev/null +++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/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