From fabebee58e55b52bb509dabc382ac3a67abd79f7 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 7 Apr 2020 14:06:38 -0400 Subject: [PATCH 1/3] [java-source-utils] Add Java source code utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/javaparser/javaparser/tree/javaparser-parent-3.16.1 There are two parts of the current `.jar` binding toolchain which are painful and could be improved: 1. Parameter names 2. Documentation extraction Parameter names (1) are important because they become the names of event members as part of ["event-ification"][0]. As such they are semantically important, and the default behavior of "p0" makes for a terrible user experience. *If* the `.class` files in the `.jar` file are built with `javac -parameters` (4273e5ce), then the `.class` file will contain parameter names and we're good. However, this may not be the case. If the `.class` files are built with `javac -g`, then we'll try to deduce parameter names from debug info, but that's also problematic. What else can be used to provide parameter names? It is not unusual for Java libraries to provide "source `.jar`" files, e.g. Android provides `android-stubs-src.jar` files, and other libraries may provide a `*-sources.jar` file. The contents of these files are *Java source code*. These files are used by Android IDEs to provide documentation for the Java library. They contain classes, methods, parameter names, and associated Javadoc documentation. What they are *not* guaranteed to do is *compile*. As such, we can't compile them ourselves with `javac -parameters` and then process the `.class` files, as they may refer to unresolvable types. "Interestingly", we *already* have some tooling to deal with this: `tools/param-name-importer` uses a custom Irony grammar to parse the Android SDK `*-stubs-src.jar` files to grab parameter names. However, this tooling is *too strict*; try to pass arbitrary Java source code at it, and it quickly fails. Which brings us to documentation (2): we have a [javadoc2mdoc][1] tool which will parse Javadoc HTML documentation and convert it into [**mdoc**(5)][2] documentation, which can be later turned into [XML documentation comments][3] files by way of [**mdoc export-msxdoc**(1)][4], but this tool is (1) painful to maintain, because it processes Javadoc *HTML*, and (2) *requires Javadoc HTML*. Google hasn't updated their downloadable Javadoc `.zip` file since API-24 (2016-October). API-30 is currently stable. If we want newer docs, we either need to scrape the developer.android.com/reference website to use with the existing tooling, or... we need to be able to read the Javadoc comments within the `*-stubs-src.jar` files provided with the Android SDK. (Note: Android SDK docs are Apache 2; file format conversion is fine.) We thus have two use-cases for which parsing Java source code would be useful.. As luck would have it, there's a decent Apache 2-licensed Java project which supports parsing Java source code: [JavaParser][5]. Add a new `tools/java-source-utils` program which will parse Java source code to produce two artifacts: parameter names and consolidated Javadoc documentation: $ java -jar java-source-utils.jar --help java-source-utils [-v] [<-a|--aar> AAR]* [<-j|--jar> JAR]* [<-s|--source> DIRS]* [--bootclasspath CLASSPATH] [<-P|--output-params> OUT.params.txt] [<-D|--output-javadoc> OUT.xml] FILES Provide `--output-params OUT.params.txt`, and the specified file will be created which follows the file format laid out in [`JavaParameterNamesLoader.cs`][6]: package java.lang ;--------------------------------------- class Object wait(long timeout) Provide `--output-javadocs OUT.xml`, and the resulting file will be a `class-parse`-like XML file which uses `//@jni-signature` as the "key" and a child `` element to contain documentation, e.g.: This should make it possible to update the Xamarin.Android API documentation without resorting to web scraping (and updating the code to deal with whatever new HTML dialects are now used). If neither `--output-params` nor `--output-javadocs` is used, then `--output-javadocs` will be executed, writing to stdout. The XML file *also* contains parameter name information, so that one file can be the "source of truth" for parameter names and documentation. `FILES` can be: * Java source code in a `.java` file; or * A file with a `.jar` or `.zip` extension, which will be extracted into a temp directory and all `.java` files within the directory will be processed; or * A directory tree, and all `.java` files will be processed. If a single file references other types, the "root" directory containing those types may need to be specified via `--source DIR`: $ java -jar "bin/Debug/java-source-utils.jar" -v \ -s $HOMEandroid-toolchain/sdk/platforms/_t \ $HOME/android-toolchain/sdk/platforms/_t/android/app/Activity.java \ -P android.params.txt -D android.xml >o.txt 2>&1 TODO: In some scenarios, types won't be resolvable. What should output be? We don't want to *require* that everything be resolvable -- it's painful, and possibly impossible, e.g. w/ internal types -- so instead we should "demark" the unresolvable types. `.params.txt` output will use `.*` as a type prefix, e.g. method(.*UnresolvableType foo, int bar); `docs.xml` will output `L.*UnresolvableType;`. Fix JavaParameterNamesLoader.cs to support the above. [0]: https://docs.microsoft.com/en-us/xamarin/android/internals/api-design#events-and-listeners [1]: https://github.com/xamarin/xamarin-android/tree/d48cf04f9749664bf48fc16bcb920d5d941cccab/tools/javadoc2mdoc [2]: http://docs.go-mono.com/?link=man%3amdoc(5) [3]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/ [4]: http://docs.go-mono.com/?link=man%3amdoc-export-msxdoc(1) [5]: https://javaparser.org [6]: https://github.com/xamarin/java.interop/blob/93df5a200e7b6f1b5add451aff66bbcb24293720/src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs#L45-L68 --- .editorconfig | 2 +- .vscode/settings.json | 1 + Directory.Build.props | 3 + Java.Interop.code-workspace | 4 +- Java.Interop.sln | 7 + Makefile | 4 + .../Java.Interop.BootstrapTasks/JdkInfo.cs | 13 +- .../gradle/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + build-tools/gradle/gradlew | 172 ++++++++ build-tools/gradle/gradlew.bat | 84 ++++ .../java/com/xamarin/JavaType.java | 71 +++ tools/java-source-utils/.classpath | 26 ++ tools/java-source-utils/.gitignore | 10 + tools/java-source-utils/.project | 23 + tools/java-source-utils/CGManifest.json | 26 ++ .../java-source-utils/Directory.Build.targets | 102 +++++ tools/java-source-utils/README.md | 8 + tools/java-source-utils/build.gradle | 56 +++ .../java-source-utils.csproj | 33 ++ tools/java-source-utils/settings.gradle | 10 + .../main/java/com/microsoft/android/App.java | 74 ++++ .../android/JavaSourceUtilsOptions.java | 257 +++++++++++ .../android/JavadocXmlGenerator.java | 174 ++++++++ .../android/JniPackagesInfoFactory.java | 414 ++++++++++++++++++ .../android/ParameterNameGenerator.java | 103 +++++ .../android/ast/HasJavadocComment.java | 6 + .../microsoft/android/ast/JniClassInfo.java | 12 + .../android/ast/JniConstructorInfo.java | 17 + .../microsoft/android/ast/JniFieldInfo.java | 26 ++ .../android/ast/JniInterfaceInfo.java | 12 + .../microsoft/android/ast/JniMemberInfo.java | 49 +++ .../android/ast/JniMethodBaseInfo.java | 36 ++ .../microsoft/android/ast/JniMethodInfo.java | 64 +++ .../microsoft/android/ast/JniPackageInfo.java | 47 ++ .../android/ast/JniPackagesInfo.java | 37 ++ .../android/ast/JniParameterInfo.java | 21 + .../microsoft/android/ast/JniTypeInfo.java | 93 ++++ .../com/microsoft/android/util/Parameter.java | 27 ++ .../android/JavaSourceUtilsOptionsTest.java | 20 + .../android/JavadocXmlGeneratorTest.java | 125 ++++++ .../android/JniPackagesInfoFactoryTest.java | 59 +++ .../android/JniPackagesInfoTest.java | 82 ++++ .../android/ParameterNameGeneratorTest.java | 93 ++++ .../src/test/resources/com/.gitignore | 1 + .../com/microsoft/android/.gitignore | 1 + .../com/microsoft/android/JavaType.params.txt | 21 + .../com/microsoft/android/JavaType.xml | 174 ++++++++ .../com/microsoft/android/Outer.java | 121 +++++ .../com/microsoft/android/Outer.params.txt | 12 + .../resources/com/microsoft/android/Outer.xml | 79 ++++ 51 files changed, 2911 insertions(+), 6 deletions(-) create mode 100644 build-tools/gradle/gradle/wrapper/gradle-wrapper.jar create mode 100644 build-tools/gradle/gradle/wrapper/gradle-wrapper.properties create mode 100755 build-tools/gradle/gradlew create mode 100644 build-tools/gradle/gradlew.bat create mode 100644 tools/java-source-utils/.classpath create mode 100644 tools/java-source-utils/.gitignore create mode 100644 tools/java-source-utils/.project create mode 100644 tools/java-source-utils/CGManifest.json create mode 100644 tools/java-source-utils/Directory.Build.targets create mode 100644 tools/java-source-utils/README.md create mode 100644 tools/java-source-utils/build.gradle create mode 100644 tools/java-source-utils/java-source-utils.csproj create mode 100644 tools/java-source-utils/settings.gradle create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/App.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java create mode 100644 tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java create mode 100644 tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java create mode 100644 tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java create mode 100644 tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java create mode 100644 tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java create mode 100644 tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java create mode 100644 tools/java-source-utils/src/test/resources/com/.gitignore create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt create mode 100644 tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml diff --git a/.editorconfig b/.editorconfig index 73178936a..7d06415bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,7 +30,7 @@ indent_style = space indent_size = 4 # Code files -[*.{cs,csx,vb,vbx}] +[*.{cs,csx,java,vb,vbx}] insert_final_newline = true indent_style = tab tab_width = 8 diff --git a/.vscode/settings.json b/.vscode/settings.json index 98c263e55..ae4867421 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "java.configuration.updateBuildConfiguration": "automatic", "nxunitExplorer.nunit": "packages/NUnit.ConsoleRunner.3.9.0/tools/nunit3-console.exe", "nxunitExplorer.modules": [ "bin/TestDebug/generator-Tests.dll", diff --git a/Directory.Build.props b/Directory.Build.props index 2d850d6a5..70cc83903 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -43,6 +43,9 @@ $(MSBuildThisFileDirectory)external\xamarin-android-tools + $(MSBuildThisFileDirectory)build-tools\gradle + $(GradleHome)\gradlew + --stacktrace --no-daemon 1.8 1.8 <_BootClassPath Condition=" '$(JreRtJarPath)' != '' ">-bootclasspath "$(JreRtJarPath)" diff --git a/Java.Interop.code-workspace b/Java.Interop.code-workspace index 32e45d9f7..7ad00c311 100644 --- a/Java.Interop.code-workspace +++ b/Java.Interop.code-workspace @@ -4,5 +4,7 @@ "path": "." } ], - "settings": {} + "settings": { + "java.configuration.updateBuildConfiguration": "interactive" + } } \ No newline at end of file diff --git a/Java.Interop.sln b/Java.Interop.sln index 7ad9c60b7..b1715974c 100644 --- a/Java.Interop.sln +++ b/Java.Interop.sln @@ -91,6 +91,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Java.Interop.Tools.Generato EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.Generator-Tests", "tests\Java.Interop.Tools.Generator-Tests\Java.Interop.Tools.Generator-Tests.csproj", "{7F4828AB-3908-458C-B09F-33C74A1368F9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "java-source-utils", "tools\java-source-utils\java-source-utils.csproj", "{F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5 @@ -251,6 +253,10 @@ Global {7F4828AB-3908-458C-B09F-33C74A1368F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F4828AB-3908-458C-B09F-33C74A1368F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F4828AB-3908-458C-B09F-33C74A1368F9}.Release|Any CPU.Build.0 = Release|Any CPU + {F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -294,6 +300,7 @@ Global {0E3AF6C1-7638-464D-9174-485D494499DC} = {C8F58966-94BF-407F-914A-8654F8B8AE3B} {C2FD2F12-DE3B-4FB9-A0D3-FA3EF597DD04} = {0998E45F-8BCE-4791-A944-962CD54E2D80} {7F4828AB-3908-458C-B09F-33C74A1368F9} = {271C9F30-F679-4793-942B-0D9527CB3E2F} + {F46EDFA5-C52A-4F0C-B5A2-5BB67E0D8C74} = {C8F58966-94BF-407F-914A-8654F8B8AE3B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5} diff --git a/Makefile b/Makefile index 6da9146fa..ba9516b2e 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ run-all-tests: $(MAKE) run-tests || r=1 ; \ $(MAKE) run-test-jnimarshal || r=1 ; \ $(MAKE) run-ptests || r=1 ; \ + $(MAKE) run-java-source-utils-tests || r=1 ; \ exit $$r; include build-tools/scripts/msbuild.mk @@ -144,6 +145,9 @@ run-ptests: $(PTESTS) bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB) $(foreach t,$(PTESTS), $(call RUN_TEST,$(t))) \ exit $$r; +run-java-source-utils-tests: + $(MSBUILD) $(MSBUILD_FLAGS) tools/java-source-utils/java-source-utils.csproj /t:RunTests + bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB): bin/$(CONFIGURATION)/$(JAVA_INTEROP_LIB) cp $< $@ diff --git a/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs b/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs index ea789d86b..31acad96a 100644 --- a/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs +++ b/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs @@ -56,8 +56,8 @@ public override bool Execute () Directory.CreateDirectory (Path.GetDirectoryName (PropertyFile.ItemSpec)); Directory.CreateDirectory (Path.GetDirectoryName (MakeFragmentFile.ItemSpec)); - WritePropertyFile (jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath); - WriteMakeFragmentFile (jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath); + WritePropertyFile (jdk.JavaPath, jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath); + WriteMakeFragmentFile (jdk.JavaPath, jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath); return !Log.HasLoggedErrors; } @@ -93,7 +93,7 @@ Action CreateLogger () return logger; } - void WritePropertyFile (string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable includes) + void WritePropertyFile (string javaPath, string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable includes) { var msbuild = XNamespace.Get ("http://schemas.microsoft.com/developer/msbuild/2003"); var project = new XElement (msbuild + "Project", @@ -104,6 +104,10 @@ void WritePropertyFile (string jarPath, string javacPath, string jdkJvmPath, str new XElement (msbuild + "ItemGroup", includes.Select (i => new XElement (msbuild + "JdkIncludePath", new XAttribute ("Include", i)))))), new XElement (msbuild + "PropertyGroup", + new XElement (msbuild + "JavaSdkDirectory", new XAttribute ("Condition", " '$(JavaSdkDirectory)' == '' "), + JavaHomePath), + new XElement (msbuild + "JavaPath", new XAttribute ("Condition", " '$(JavaPath)' == '' "), + javaPath), new XElement (msbuild + "JavaCPath", new XAttribute ("Condition", " '$(JavaCPath)' == '' "), javacPath), new XElement (msbuild + "JarPath", new XAttribute ("Condition", " '$(JarPath)' == '' "), @@ -121,10 +125,11 @@ static XElement CreateJreRtJarPath (XNamespace msbuild, string rtJarPath) rtJarPath); } - void WriteMakeFragmentFile (string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable includes) + void WriteMakeFragmentFile (string javaPath, string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable includes) { using (var o = new StreamWriter (MakeFragmentFile.ItemSpec)) { o.WriteLine ($"export JI_JAR_PATH := {jarPath}"); + o.WriteLine ($"export JI_JAVA_PATH := {javaPath}"); o.WriteLine ($"export JI_JAVAC_PATH := {javacPath}"); o.WriteLine ($"export JI_JDK_INCLUDE_PATHS := {string.Join (" ", includes)}"); o.WriteLine ($"export JI_JVM_PATH := {jdkJvmPath}"); diff --git a/build-tools/gradle/gradle/wrapper/gradle-wrapper.jar b/build-tools/gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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 \(.*\)$'` + 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='"-Xmx64m"' + +# 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/build-tools/gradle/gradlew.bat b/build-tools/gradle/gradlew.bat new file mode 100644 index 000000000..6d57edc70 --- /dev/null +++ b/build-tools/gradle/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="-Xmx64m" + +@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/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java b/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java index 34f2d3e28..baad63fb1 100644 --- a/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java @@ -3,37 +3,76 @@ import java.util.ArrayList; import java.util.List; +/** + * JNI sig: Lcom/xamarin/JavaEnum; + */ enum JavaEnum { + /** FIRST; JNI sig: Lcom/xamarin/JavaEnum; */ FIRST, + + /** SECOND; JNI sig: Lcom/xamarin/JavaEnum; */ + SECOND; + /** + * summary + * + *

Paragraphs of text? + * + * @return some value + */ public int switchValue() { return 0; } } +/** + * JNI sig: Lcom/xamarin/JavaType; + * + * @param + */ + public class JavaType implements Cloneable, Comparable>, IJavaInterface, List> { + /** JNI sig: STATIC_FINAL_OBJECT.L/java/lang/Object; */ + @Deprecated public static final Object STATIC_FINAL_OBJECT = new Object (); + /** JNI sig: STATIC_FINAL_INT32.I */ public static final int STATIC_FINAL_INT32 = 42; + /** JNI sig: STATIC_FINAL_INT32_MIN.I */ public static final int STATIC_FINAL_INT32_MIN = Integer.MIN_VALUE; + /** JNI sig: STATIC_FINAL_INT32_MAX.I */ public static final int STATIC_FINAL_INT32_MAX = Integer.MAX_VALUE; + /** JNI sig: STATIC_FINAL_CHAR_MIN.C */ public static final char STATIC_FINAL_CHAR_MIN = Character.MIN_VALUE; + /** JNI sig: STATIC_FINAL_CHAR_MAX.C */ public static final char STATIC_FINAL_CHAR_MAX = Character.MAX_VALUE; + /** JNI sig: STATIC_FINAL_INT64_MIN.J */ public static final long STATIC_FINAL_INT64_MIN = Long.MIN_VALUE; + /** JNI sig: STATIC_FINAL_INT64_MAX.J */ public static final long STATIC_FINAL_INT64_MAX = Long.MAX_VALUE; + /** JNI sig: STATIC_FINAL_SINGLE_MIN.F */ public static final float STATIC_FINAL_SINGLE_MIN = Float.MIN_VALUE; + /** JNI sig: STATIC_FINAL_SINGLE_MAX.F */ public static final float STATIC_FINAL_SINGLE_MAX = Float.MAX_VALUE; + /** JNI sig: STATIC_FINAL_DOUBLE_MIN.D */ public static final double STATIC_FINAL_DOUBLE_MIN = Double.MIN_VALUE; + /** JNI sig: STATIC_FINAL_DOUBLE_MAX.D */ public static final double STATIC_FINAL_DOUBLE_MAX = Double.MAX_VALUE; + /** JNI sig: STATIC_FINAL_STRING.Ljava/lang/String; */ public static final String STATIC_FINAL_STRING = "Hello, \\\"embedded\u0000Nulls\" and \uD83D\uDCA9!"; + /** JNI sig: STATIC_FINAL_BOOL_FALSE.Z */ public static final boolean STATIC_FINAL_BOOL_FALSE = false; + /** JNI sig: STATIC_FINAL_BOOL_TRUE.Z */ public static final boolean STATIC_FINAL_BOOL_TRUE = true; + /** JNI sig: POSITIVE_INFINITY.D */ public static final double POSITIVE_INFINITY = 1.0 / 0.0; + /** JNI sig: NEGATIVE_INFINITY.D */ public static final double NEGATIVE_INFINITY = -1.0 / 0.0; + /** JNI sig: NaN.D */ public static final double NaN = 0.0d / 0.0; @@ -51,60 +90,86 @@ public class JavaType // N: Non-static inner class // C: Class // I: Interface + + /** JNI sig: Lcom/xamarin/JavaType$PSC; */ + public static abstract class PSC { } + /** JNI sig: Lcom/xamarin/JavaType$RNC; */ protected abstract class RNC { + /** JNI sig: ()V */ protected RNC () { } + /** JNI sig: (Ljava/lang/Object;Ljava/lang/Object;)V */ protected RNC (E value1, E2 value2) { } + /** JNI sig: (Ljava/lang/Object;)Ljava/lang/Object; */ + public abstract E2 fromE (E value); + /** JNI sig: Lcom/xamarin/JavaType$RNC$RPNC; */ public abstract class RPNC { + /** JNI sig: ()V */ public RPNC () { } + /** JNI sig: (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V */ public RPNC (E value1, E2 value2, E3 value3) { } + /** JNI sig: fromE2.(Ljava/lang/Object;)Ljava/lang/Object; */ public abstract E3 fromE2 (E2 value); } } + /** JNI sig: Lcom/xamarin/JavaType$ASC; */ + @Deprecated /* package */ static class ASC { } + /** JNI sig: ()V */ public JavaType () { } + /** JNI sig: (Ljava/lang/String;)V */ public JavaType (String value) { } + /** JNI sig: INSTANCE_FINAL_OBJECT.Ljava/lang/Object; */ @Deprecated public final Object INSTANCE_FINAL_OBJECT = new Object (); + + /** JNI sig: INSTANCE_FINAL_E.Ljava/lang/Object; */ public final E INSTANCE_FINAL_E = null; + /** JNI sig: packageInstanceEArray.[Ljava/lang/Object; */ /* package */ E[] packageInstanceEArray; + + /** JNI sig: protectedInstanceEList.Ljava/util/List; */ protected List protectedInstanceEList; private List[] privateInstanceArrayOfListOfIntArrayArray; + /** JNI sig: compareTo.(Lcom/xamarin/JavaType;)I */ public int compareTo (JavaType value) { return 0; } + /** JNI sig: func.(Ljava/lang/StringBuilder;)Ljava/util/List; */ public List func (StringBuilder value) { return null; } + /** JNI sig: run.()V */ public void run () { } + /** JNI sig: action.(Ljava/lang/Object;)V */ @Deprecated public void action (Object value) { Object local = new Object (); @@ -118,10 +183,12 @@ public void run() { r.run(); } + /** JNI sig: func.([Ljava/lang/String;)Ljava/lang/Integer; */ public java.lang.Integer func (String[] values) { return values.length; } + /** JNI sig: staticActionWithGenerics.(Ljava/lang/Object;Ljava/lang/Number;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V */ public static , TThrowable extends Throwable> void staticActionWithGenerics ( T value1, @@ -132,19 +199,23 @@ void staticActionWithGenerics ( throws IllegalArgumentException, NumberFormatException, TThrowable { } + /** JNI sig: instanceActionWithGenerics.(Ljava/lang/Object;java/lang/Object;)V */ public void instanceActionWithGenerics ( T value1, E value2) { } + /** JNI sig: sum.(I[I)I */ public static int sum (int first, int... remaining) { return -1; } + /** JNI sig: finalize.()V */ protected void finalize () { } + /** JNI sig: finalize.(I)I */ public static int finalize (int value) { return value; } diff --git a/tools/java-source-utils/.classpath b/tools/java-source-utils/.classpath new file mode 100644 index 000000000..30230c1c4 --- /dev/null +++ b/tools/java-source-utils/.classpath @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/java-source-utils/.gitignore b/tools/java-source-utils/.gitignore new file mode 100644 index 000000000..5e4e31eb4 --- /dev/null +++ b/tools/java-source-utils/.gitignore @@ -0,0 +1,10 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# ??? +.settings + +# Ignore Gradle build output directory +build + + diff --git a/tools/java-source-utils/.project b/tools/java-source-utils/.project new file mode 100644 index 000000000..8337a1c56 --- /dev/null +++ b/tools/java-source-utils/.project @@ -0,0 +1,23 @@ + + + java-source-utils + Project java-source-utils created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/tools/java-source-utils/CGManifest.json b/tools/java-source-utils/CGManifest.json new file mode 100644 index 000000000..eeac90a8f --- /dev/null +++ b/tools/java-source-utils/CGManifest.json @@ -0,0 +1,26 @@ +{ + "Registrations": [ + { + "Component": { + "Type": "maven", + "Maven": { + "GroupId": "com.github.javaparser", + "ArtifactId": "javaparser-core", + "Version": "3.16.1" + } + }, + "DevelopmentDependency":false + }, + { + "Component": { + "Type": "maven", + "Maven": { + "GroupId": "com.github.javaparser", + "ArtifactId": "javaparser-symbol-solver-core", + "Version": "3.16.1" + } + }, + "DevelopmentDependency":false + } + ] +} \ No newline at end of file diff --git a/tools/java-source-utils/Directory.Build.targets b/tools/java-source-utils/Directory.Build.targets new file mode 100644 index 000000000..2ef10a27e --- /dev/null +++ b/tools/java-source-utils/Directory.Build.targets @@ -0,0 +1,102 @@ + + + + + + + + + + + + + <_XATBJavaSourceFile Include="JavaType.java" /> + <_XATBJavaSource Include="@(_XATBJavaSourceFile->'$(MSBuildThisFileDirectory)../../tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/%(Identity)')" /> + <_XATBJavaDest Include="@(_XATBJavaSourceFile->'$(MSBuildThisFileDirectory)src/test/resources/com/xamarin/%(Identity)')" /> + + + + + <_Dirs Include="@(_XATBJavaDest->'%(RelativeDir)')" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_JavaSource Include="src/test/resources/com/microsoft/android/Outer.java" /> + <_JavaSource Include="src/test/resources/com/xamarin/JavaType.java" /> + + + + + + + + + diff --git a/tools/java-source-utils/README.md b/tools/java-source-utils/README.md new file mode 100644 index 000000000..9a845f808 --- /dev/null +++ b/tools/java-source-utils/README.md @@ -0,0 +1,8 @@ +# java-source-utils + +`java-source-utils` is a Java program which uses [JavaParser][0] to process +Java source code in order to extract method parameter names and Javadoc +documentation, as the typical alternative is to instead process Javadoc *HTML* +to obtain this information, and Javadoc HTML is less "stable" than Java source. + +[0]: https://github.com/javaparser/javaparser diff --git a/tools/java-source-utils/build.gradle b/tools/java-source-utils/build.gradle new file mode 100644 index 000000000..516e39b6b --- /dev/null +++ b/tools/java-source-utils/build.gradle @@ -0,0 +1,56 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * User Manual available at https://docs.gradle.org/6.3/userguide/tutorial_java_projects.html + */ + +plugins { + // Apply the java plugin to add support for Java + id 'java' + + // Apply the application plugin to add support for building a CLI application. + id 'application' +} + +java { + ext.javaSourceVer = project.hasProperty('javaSourceVer') ? JavaVersion.toVersion(project.getProperty('javaSourceVer')) : JavaVersion.VERSION_1_8 + ext.javaTargetVer = project.hasProperty('javaTargetVer') ? JavaVersion.toVersion(project.getProperty('javaTargetVer')) : JavaVersion.VERSION_1_8 + + sourceCompatibility = ext.javaSourceVer + targetCompatibility = ext.javaTargetVer +} + +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is used by the application. + implementation 'com.github.javaparser:javaparser-core:3.16.1' + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.16.1' + + // Use JUnit test framework + testImplementation 'junit:junit:4.12' +} + +application { + // Define the main class for the application. + mainClassName = 'com.microsoft.android.App' +} + +jar { + duplicatesStrategy = 'exclude' + manifest { + attributes 'Main-Class': 'com.microsoft.android.App' + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' + } + archiveName 'java-source-utils.jar' +} diff --git a/tools/java-source-utils/java-source-utils.csproj b/tools/java-source-utils/java-source-utils.csproj new file mode 100644 index 000000000..dfd448b0e --- /dev/null +++ b/tools/java-source-utils/java-source-utils.csproj @@ -0,0 +1,33 @@ + + + + + net472;netcoreapp3.1 + java-source-utils.jar + .jar + jar + false + none + + + + $(UtilityOutputFullPath) + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/java-source-utils/settings.gradle b/tools/java-source-utils/settings.gradle new file mode 100644 index 000000000..8a5eebc59 --- /dev/null +++ b/tools/java-source-utils/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.3/userguide/multi_project_builds.html + */ + +rootProject.name = 'java-source-utils' diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/App.java b/tools/java-source-utils/src/main/java/com/microsoft/android/App.java new file mode 100644 index 000000000..b67d58c02 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/App.java @@ -0,0 +1,74 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package com.microsoft.android; + +import java.io.IOException; + +import com.github.javaparser.*; +import com.github.javaparser.ParserConfiguration; + +import com.microsoft.android.ast.*; +import com.microsoft.android.util.Parameter; + +public class App { + public static final String APP_NAME = "java-source-utils"; + + public static void main (final String[] args) throws Throwable { + JavaSourceUtilsOptions options; + try { + options = JavaSourceUtilsOptions.parse(args); + if (options == null) { + System.out.println(APP_NAME + " " + JavaSourceUtilsOptions.HELP_STRING); + return; + } + } catch (Throwable t) { + System.err.println(APP_NAME + ": error: " + t.getMessage()); + if (JavaSourceUtilsOptions.verboseOutput) { + t.printStackTrace(System.err); + } + System.err.println("Usage: " + APP_NAME + " " + JavaSourceUtilsOptions.HELP_STRING); + System.exit(1); + return; + } + + try { + final JavaParser parser = createParser(options); + final JniPackagesInfoFactory packagesFactory = new JniPackagesInfoFactory(parser); + final JniPackagesInfo packages = packagesFactory.parse(options.inputFiles); + + if ((options.outputParamsTxt = Parameter.normalize(options.outputParamsTxt, "")).length() > 0) { + generateParamsTxt(options.outputParamsTxt, packages); + } + generateXml(options.outputJavadocXml, packages); + options.close(); + } + catch (Throwable t) { + options.close(); + System.err.println(APP_NAME + ": internal error: " + t.getMessage()); + if (JavaSourceUtilsOptions.verboseOutput) { + t.printStackTrace(System.err); + } + System.exit(2); + return; + } + } + + static JavaParser createParser(JavaSourceUtilsOptions options) throws IOException { + final ParserConfiguration config = options.createConfiguration(); + final JavaParser parser = new JavaParser(config); + return parser; + } + + static void generateParamsTxt(String filename, JniPackagesInfo packages) throws Throwable { + try (final ParameterNameGenerator paramsTxtGen = new ParameterNameGenerator(filename)) { + paramsTxtGen.writePackages(packages); + } + } + + static void generateXml(String filename, JniPackagesInfo packages) throws Throwable { + try (final JavadocXmlGenerator javadocXmlGen = new JavadocXmlGenerator(filename)) { + javadocXmlGen.writePackages(packages); + } + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java new file mode 100644 index 000000000..78ec4f613 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java @@ -0,0 +1,257 @@ +package com.microsoft.android; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.github.javaparser.*; +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.*; +import com.github.javaparser.ast.type.*; +import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.nodeTypes.*; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.nodeTypes.NodeWithParameters; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.resolution.SymbolResolver; +import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.symbolsolver.*; +import com.github.javaparser.symbolsolver.model.resolution.TypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.*; + + +public class JavaSourceUtilsOptions implements AutoCloseable { + public static final String HELP_STRING = "[-v] [<-a|--aar> AAR]* [<-j|--jar> JAR]* [<-s|--source> DIRS]*\n" + + "\t[--bootclasspath CLASSPATH]\n" + + "\t[<-P|--output-params> OUT.params.txt] [<-D|--output-javadoc> OUT.xml] FILES"; + + public static boolean verboseOutput; + + public final List aarFiles = new ArrayList(); + public final List jarFiles = new ArrayList(); + + public final Collection inputFiles = new ArrayList(); + + public boolean haveBootClassPath; + public String outputParamsTxt; + public String outputJavadocXml; + + private final Collection sourceDirectoryFiles = new ArrayList(); + private File extractedTempDir; + + + public void close() { + if (extractedTempDir != null) { + try { + Files.walk(extractedTempDir.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + extractedTempDir.delete(); + } + catch (Throwable t) { + System.err.println(App.APP_NAME + ": error deleting temp directory `" + extractedTempDir.getAbsolutePath() + "`: " + t.getMessage()); + if (verboseOutput) { + t.printStackTrace(System.err); + } + } + } + extractedTempDir = null; + } + + public ParserConfiguration createConfiguration() throws IOException { + final ParserConfiguration config = new ParserConfiguration() + // Associate Javadoc comments with AST members + .setAttributeComments(true) + + // If there are blank lines between Javadoc blocks & declarations, + // *ignore* those blank lines and associate the Javadoc w/ the decls + .setDoNotAssignCommentsPrecedingEmptyLines(false) + + // Associate Javadoc comments w/ the declaration, *not* with + // any annotations on the declaration + .setIgnoreAnnotationsWhenAttributingComments(true) + ; + final TypeSolver typeSolver = createTypeSolver(config); + config.setSymbolResolver(new JavaSymbolSolver(typeSolver)); + return config; + } + + private final TypeSolver createTypeSolver(ParserConfiguration config) throws IOException { + final CombinedTypeSolver typeSolver = new CombinedTypeSolver(); + for (File file : aarFiles) { + typeSolver.add(new AarTypeSolver(file)); + } + for (File file : jarFiles) { + typeSolver.add(new JarTypeSolver(file)); + } + if (!haveBootClassPath) { + typeSolver.add(new ReflectionTypeSolver()); + } + for (File srcDir : sourceDirectoryFiles) { + typeSolver.add(new JavaParserTypeSolver(srcDir, config)); + } + return typeSolver; + } + + public static JavaSourceUtilsOptions parse(final String[] args) throws IOException { + final JavaSourceUtilsOptions options = new JavaSourceUtilsOptions(); + + for (int i = 0; i < args.length; ++i) { + final String arg = args[i]; + switch (arg) { + case "-bootclasspath": { + final String bootClassPath = getOptionValue(args, ++i, arg); + final ArrayList files = new ArrayList(); + for (final String cp : bootClassPath.split(File.pathSeparator)) { + final File file = new File(cp); + if (!file.exists()) { + System.err.println(App.APP_NAME + ": warning: invalid file path for option `-bootclasspath`: " + cp); + continue; + } + files.add(file); + } + for (int j = files.size(); j > 0; --j) { + options.jarFiles.add(0, files.get(j-1)); + } + options.haveBootClassPath = true; + break; + } + case "-a": + case "--aar": { + final File file = getOptionFile(args, ++i, arg); + if (file == null) { + break; + } + options.aarFiles.add(file); + break; + } + case "-j": + case "--jar": { + final File file = getOptionFile(args, ++i, arg); + if (file == null) { + break; + } + options.jarFiles.add(file); + break; + } + case "-s": + case "--source": { + final File dir = getOptionFile(args, ++i, arg); + if (dir == null) { + break; + } + options.sourceDirectoryFiles.add(dir); + break; + } + case "-D": + case "--output-javadoc": { + options.outputJavadocXml = getOptionValue(args, ++i, arg); + break; + } + case "-P": + case "--output-params": { + options.outputParamsTxt = getOptionValue(args, ++i, arg); + break; + } + case "-v": { + verboseOutput = true; + break; + } + case "-h": + case "--help": { + return null; + } + default: { + final File file = getOptionFile(args, i, "FILES"); + if (file == null) + break; + + if (file.isDirectory()) { + options.sourceDirectoryFiles.add(file); + Files.walk(file.toPath()) + .filter(f -> Files.isRegularFile(f) && f.getFileName().toString().endsWith(".java")) + .map(Path::toFile) + .forEach(f -> options.inputFiles.add(f)); + break; + } + if (file.getName().endsWith(".java")) { + options.inputFiles.add(file); + break; + } + if (!file.getName().endsWith(".jar") && !file.getName().endsWith(".zip")) { + System.err.println(App.APP_NAME + ": warning: ignoring input file `" + file.getAbsolutePath() +"`."); + break; + } + if (options.extractedTempDir == null) { + options.extractedTempDir = Files.createTempDirectory("ji-jst").toFile(); + } + File toDir = new File(options.extractedTempDir, file.getName()); + options.sourceDirectoryFiles.add(toDir); + extractTo(file, toDir, options.inputFiles); + break; + } + } + } + return options; + } + + private static void extractTo(final File zipFilePath, final File toDir, final Collection inputFiles) throws IOException { + try (final ZipFile zipFile = new ZipFile(zipFilePath)) { + Enumeration e = zipFile.entries(); + while (e.hasMoreElements()) { + final ZipEntry entry = e.nextElement(); + if (entry.isDirectory()) + continue; + if (!entry.getName().endsWith(".java")) + continue; + final File target = new File(toDir, entry.getName()); + if (verboseOutput) { + System.out.println ("# creating file: " + target.getAbsolutePath()); + } + target.getParentFile().mkdirs(); + final InputStream zipContents = zipFile.getInputStream(entry); + Files.copy(zipContents, target.toPath()); + zipContents.close(); + inputFiles.add(target); + } + } + } + + static String getOptionValue(final String[] args, final int index, final String option) { + if (index >= args.length) + throw new IllegalArgumentException( + "Expected required value for option `" + option + "` at index " + index + "."); + return args[index]; + } + + static File getOptionFile(final String[] args, final int index, final String option) { + if (index >= args.length) + throw new IllegalArgumentException( + "Expected required value for option `" + option + "` at index " + index + "."); + final String fileName = args[index]; + final File file = new File(fileName); + if (!file.exists()) { + System.err.println(App.APP_NAME + ": warning: invalid file path for option `" + option + "`: " + fileName); + return null; + } + return file; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java new file mode 100644 index 000000000..41ad934d0 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java @@ -0,0 +1,174 @@ +package com.microsoft.android; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.microsoft.android.ast.*; +import com.microsoft.android.util.Parameter; + +public final class JavadocXmlGenerator implements AutoCloseable { + + final PrintStream output; + + public JavadocXmlGenerator(final String output) throws FileNotFoundException, UnsupportedEncodingException { + if (output == null) + this.output = System.out; + else { + final File file = new File(output); + final File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + this.output = new PrintStream(file, "UTF-8"); + } + } + + public JavadocXmlGenerator(final PrintStream output) { + Parameter.requireNotNull("output", output); + + this.output = output; + } + + public void close() { + if (output != System.out) { + output.flush(); + output.close(); + } + } + + public final void writePackages(final JniPackagesInfo packages) throws ParserConfigurationException, TransformerException { + Parameter.requireNotNull("packages", packages); + + final Document document = DocumentBuilderFactory.newInstance () + .newDocumentBuilder() + .newDocument(); + final Element api = document.createElement("api"); + api.setAttribute("api-source", "java-source-utils"); + document.appendChild(api); + + for (JniPackageInfo packageInfo : packages.getSortedPackages()) { + writePackage(document, api, packageInfo); + } + + Transformer transformer = TransformerFactory.newInstance() + .newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + transformer.transform(new DOMSource(document), new StreamResult(output)); + } + + private static final void writePackage(final Document document, final Element api, final JniPackageInfo packageInfo) { + final Element packageXml = document.createElement("package"); + packageXml.setAttribute("name", packageInfo.getPackageName()); + packageXml.setAttribute("jni-name", packageInfo.getPackageName().replace(".", "/")); + api.appendChild(packageXml); + + for (JniTypeInfo typeInfo : packageInfo.getSortedTypes()) { + writeType(document, packageXml, typeInfo); + } + } + + private static final void writeType(final Document document, final Element packageXml, final JniTypeInfo typeInfo) { + final Element typeXml = document.createElement(typeInfo.getTypeKind()); + typeXml.setAttribute("name", typeInfo.getRawName()); + typeXml.setAttribute("jni-signature", getTypeJniName(typeInfo)); + packageXml.appendChild(typeXml); + + writeJavadoc(document, typeXml, typeInfo.getJavadocComment()); + + for (JniMemberInfo memberInfo : typeInfo.getSortedMembers()) { + writeMember(document, typeXml, memberInfo); + } + } + + private static String getTypeJniName(JniTypeInfo typeInfo) { + final String packageName = typeInfo.getDeclaringPackage().getPackageName(); + final StringBuilder name = new StringBuilder(); + + name.append("L"); + if (packageName.length() > 0) { + name.append(packageName.replace(".", "/")); + name.append("/"); + } + name.append(typeInfo.getRawName().replace(".", "$")); + name.append(";"); + + return name.toString(); + } + + private static final void writeJavadoc(final Document document, final Element parent, String javadoc) { + javadoc = Parameter.normalize(javadoc, ""); + + if (javadoc.length() == 0) { + return; + } + + final Element javadocXml = document.createElement("javadoc"); + parent.appendChild(javadocXml); + + javadocXml.appendChild(document.createCDATASection(javadoc)); + } + + private static void writeMember(final Document document, final Element typeXml, final JniMemberInfo memberInfo) { + JniMethodBaseInfo paramsInfo = null; + int paramsCount = 0; + if (memberInfo.isConstructor() || memberInfo.isMethod()) { + paramsInfo = (JniMethodBaseInfo) memberInfo; + paramsCount = paramsInfo.getParameters().size(); + } + final String javadoc = Parameter.normalize(memberInfo.getJavadocComment(), ""); + if (paramsCount == 0 && javadoc.length() == 0) { + return; + } + + final Element memberXml = document.createElement(getMemberXmlElement(memberInfo)); + if (!memberInfo.isConstructor()) { + memberXml.setAttribute("name", memberInfo.getName()); + } + memberXml.setAttribute("jni-signature", memberInfo.getJniSignature()); + typeXml.appendChild(memberXml); + + if (memberInfo.isMethod()) { + final JniMethodInfo methodInfo = (JniMethodInfo) memberInfo; + memberXml.setAttribute("return", methodInfo.getJavaReturnType()); + memberXml.setAttribute("jni-return", methodInfo.getJniReturnType()); + } + + if (paramsInfo != null) { + for (JniParameterInfo paramInfo : paramsInfo.getParameters()) { + final Element parameter = document.createElement("parameter"); + parameter.setAttribute("name", paramInfo.name); + parameter.setAttribute("type", paramInfo.javaType); + parameter.setAttribute("jni-type", paramInfo.jniType); + + memberXml.appendChild(parameter); + } + } + + writeJavadoc(document, memberXml, memberInfo.getJavadocComment()); + } + + private static String getMemberXmlElement(JniMemberInfo member) { + if (member.isConstructor()) + return "constructor"; + if (member.isMethod()) + return "method"; + if (member.isField()) + return "field"; + throw new Error("Don't know XML element for: " + member.toString()); + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java new file mode 100644 index 000000000..b63c3162b --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java @@ -0,0 +1,414 @@ +package com.microsoft.android; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import com.github.javaparser.*; +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.*; +import com.github.javaparser.ast.comments.*; +import com.github.javaparser.ast.type.*; +import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.nodeTypes.*; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.nodeTypes.NodeWithParameters; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.resolution.SymbolResolver; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.types.ResolvedReferenceType; +import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.symbolsolver.*; +import com.github.javaparser.symbolsolver.model.resolution.TypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.*; + +import com.github.javaparser.ParseResult; +import com.github.javaparser.ast.CompilationUnit; + +import com.microsoft.android.ast.*; + +import javassist.compiler.ast.FieldDecl; + +import static com.microsoft.android.util.Parameter.*; + +public final class JniPackagesInfoFactory { + + final JavaParser parser; + + public JniPackagesInfoFactory(final JavaParser parser) { + requireNotNull("parser", parser); + + this.parser = parser; + } + + public JniPackagesInfo parse(final Collection files) throws Throwable { + requireNotNull("files", files); + + final JniPackagesInfo packages = new JniPackagesInfo(); + + for (final File file : files) { + final ParseResult result = parser.parse(file); + final Optional unit = result.getResult(); + if (!unit.isPresent()) { + logParseErrors(file, result.getProblems()); + continue; + } + parse(packages, unit.get()); + } + + return packages; + } + + private static void logParseErrors(final File file, final List problems) { + System.err.println(App.APP_NAME + ": could not parse file `" + file.getName() + "`:"); + for (final Problem p : problems) { + System.err.print("\t"); + Optional location = p.getLocation(); + if (location.isPresent()) { + System.err.print(location.get()); + System.err.print(": "); + } + System.err.println(p.getVerboseMessage()); + if (JavaSourceUtilsOptions.verboseOutput && p.getCause().isPresent()) { + p.getCause().get().printStackTrace(System.err); + } + } + } + + /** parse method */ + private void parse(final JniPackagesInfo packages, final CompilationUnit unit) throws Throwable { + final String packageName = unit.getPackageDeclaration().isPresent() + ? unit.getPackageDeclaration().get().getNameAsString() + : ""; + final JniPackageInfo packageInfo = packages.getPackage(packageName); + + for (final TypeDeclaration type : unit.getTypes()) { + if (JavaSourceUtilsOptions.verboseOutput && type.getFullyQualifiedName().isPresent()) { + System.out.println("Processing: " + type.getFullyQualifiedName().get()); + } + if (type.isAnnotationDeclaration()) { + final AnnotationDeclaration annoDecl = type.asAnnotationDeclaration(); + final JniTypeInfo annoInfo = createAnnotationInfo(packageInfo, annoDecl, null); + parseType(packageInfo, annoInfo, annoDecl); + continue; + } + if (type.isClassOrInterfaceDeclaration()) { + final ClassOrInterfaceDeclaration typeDecl = type.asClassOrInterfaceDeclaration(); + final JniTypeInfo typeInfo = createTypeInfo(packageInfo, typeDecl, null); + parseType(packageInfo, typeInfo, typeDecl); + continue; + } + if (type.isEnumDeclaration()) { + final EnumDeclaration enumDecl = type.asEnumDeclaration(); + final JniTypeInfo nestedEnum = createEnumInfo(packageInfo, enumDecl, null); + parseType(packageInfo, nestedEnum, enumDecl); + continue; + } + System.out.println("# TODO: unknown type decl " + type.getClass().getName()); + System.out.println(type.toString()); + } + } + + static JniTypeInfo createAnnotationInfo(final JniPackageInfo packageInfo, final AnnotationDeclaration annotationDecl, JniTypeInfo declInfo) { + final String declName = declInfo == null ? "" : declInfo.getRawName() + "."; + final JniTypeInfo annotationInfo = new JniInterfaceInfo(packageInfo, declName + annotationDecl.getNameAsString()); + packageInfo.add(annotationInfo); + fillJavadoc(annotationInfo, annotationDecl); + if (declInfo != null) { + for (String typeParameter : declInfo.getTypeParameters()) { + annotationInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter)); + } + } + return annotationInfo; + } + + static JniTypeInfo createEnumInfo(final JniPackageInfo packageInfo, final EnumDeclaration enumDecl, JniTypeInfo declInfo) { + final String declName = declInfo == null ? "" : declInfo.getRawName() + "."; + final JniTypeInfo enumInfo = new JniClassInfo(packageInfo, declName + enumDecl.getNameAsString()); + packageInfo.add(enumInfo); + fillJavadoc(enumInfo, enumDecl); + if (declInfo != null) { + for (String typeParameter : declInfo.getTypeParameters()) { + enumInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter)); + } + } + return enumInfo; + } + + static JniTypeInfo createTypeInfo(final JniPackageInfo packageInfo, final ClassOrInterfaceDeclaration typeDecl, JniTypeInfo declInfo) { + final String declName = declInfo == null ? "" : declInfo.getRawName() + "."; + final JniTypeInfo typeInfo = typeDecl.isInterface() + ? new JniInterfaceInfo(packageInfo, declName + typeDecl.getNameAsString()) + : new JniClassInfo(packageInfo, declName + typeDecl.getNameAsString()); + packageInfo.add(typeInfo); + fillJavadoc(typeInfo, typeDecl); + if (declInfo != null) { + for (String typeParameter : declInfo.getTypeParameters()) { + typeInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter)); + } + } + for (TypeParameter typeParameter : typeDecl.getTypeParameters()) { + typeInfo.addTypeParameter( + typeParameter.getNameAsString(), + getJniType(typeInfo, null, getTypeParameterBound(typeParameter))); + } + return typeInfo; + } + + static ClassOrInterfaceType getTypeParameterBound(TypeParameter typeParameter) { + for (ClassOrInterfaceType boundType : typeParameter.getTypeBound()) { + return boundType; + } + return null; + } + + private final void parseType(final JniPackageInfo packageInfo, final JniTypeInfo typeInfo, TypeDeclaration typeDecl) { + for (final BodyDeclaration body : typeDecl.getMembers()) { + if (body.isAnnotationDeclaration()) { + final AnnotationDeclaration annoDecl = body.asAnnotationDeclaration(); + final JniTypeInfo annoInfo = createAnnotationInfo(packageInfo, annoDecl, typeInfo); + parseType(packageInfo, annoInfo, annoDecl); + continue; + } + if (body.isClassOrInterfaceDeclaration()) { + final ClassOrInterfaceDeclaration nestedDecl = body.asClassOrInterfaceDeclaration(); + final JniTypeInfo nestedType = createTypeInfo(packageInfo, nestedDecl, typeInfo); + parseType(packageInfo, nestedType, nestedDecl); + continue; + } + if (body.isEnumDeclaration()) { + final EnumDeclaration enumDecl = body.asEnumDeclaration(); + final JniTypeInfo nestedEnum = createEnumInfo(packageInfo, enumDecl, typeInfo); + parseType(packageInfo, nestedEnum, enumDecl); + continue; + } + if (body.isAnnotationMemberDeclaration()) { + parseAnnotationMemberDecl(typeInfo, body.asAnnotationMemberDeclaration()); + continue; + } + if (body.isConstructorDeclaration()) { + parseConstructorDecl(typeInfo, body.asConstructorDeclaration()); + continue; + } + if (body.isFieldDeclaration()) { + parseFieldDecl(typeInfo, body.asFieldDeclaration()); + continue; + } + if (body.isMethodDeclaration()) { + parseMethodDecl(typeInfo, body.asMethodDeclaration()); + continue; + } + if (body.isInitializerDeclaration()) { + // e.g. `static { CREATOR = null; } + continue; + } + System.out.println("# TODO: unknown body member " + body.getClass().getName()); + System.out.println(body.toString()); + } + } + + private final void parseAnnotationMemberDecl(final JniTypeInfo typeInfo, final AnnotationMemberDeclaration memberDecl) { + final JniMethodInfo methodInfo = new JniMethodInfo(typeInfo, memberDecl.getNameAsString()); + typeInfo.add(methodInfo); + + methodInfo.setReturnType( + getJavaType(typeInfo, methodInfo, memberDecl.getType()), + getJniType(typeInfo, methodInfo, memberDecl.getType())); + + fillJavadoc(methodInfo, memberDecl); + } + + private final void parseFieldDecl(final JniTypeInfo typeInfo, final FieldDeclaration fieldDecl) { + for (VariableDeclarator f : fieldDecl.getVariables()) { + final JniFieldInfo fieldInfo = new JniFieldInfo(typeInfo, f.getNameAsString()); + fieldInfo.setJniType(getJniType(typeInfo, null, f.getType())); + + typeInfo.add(fieldInfo); + + fillJavadoc(fieldInfo, fieldDecl); + } + } + + private final void parseConstructorDecl(final JniTypeInfo typeInfo, final ConstructorDeclaration ctorDecl) { + final JniConstructorInfo ctorInfo = new JniConstructorInfo(typeInfo); + typeInfo.add(ctorInfo); + + fillMethodBase(ctorInfo, ctorDecl); + fillJavadoc(ctorInfo, ctorDecl); + } + + private final void parseMethodDecl(final JniTypeInfo typeInfo, final MethodDeclaration methodDecl) { + final JniMethodInfo methodInfo = new JniMethodInfo(typeInfo, methodDecl.getNameAsString()); + typeInfo.add(methodInfo); + + for (TypeParameter typeParameter : methodDecl.getTypeParameters()) { + methodInfo.addTypeParameter( + typeParameter.getNameAsString(), + getJniType(typeInfo, methodInfo, getTypeParameterBound(typeParameter))); + } + methodInfo.setReturnType( + getJavaType(typeInfo, methodInfo, methodDecl.getType()), + getJniType(typeInfo, methodInfo, methodDecl.getType())); + + fillMethodBase(methodInfo, methodDecl); + fillJavadoc(methodInfo, methodDecl); + } + + private static final void fillJavadoc(final HasJavadocComment member, NodeWithJavadoc nodeWithJavadoc) { + JavadocComment javadoc = null; + if (nodeWithJavadoc.getJavadocComment().isPresent()) { + javadoc = nodeWithJavadoc.getJavadocComment().get(); + } else { + Node node = (Node) nodeWithJavadoc; + if (!node.getParentNode().isPresent()) + return; + + /* + * Sometimes `JavaParser` won't associate a Javadoc comment block with + * the AST node we expect it to. In such circumstances the Javadoc + * comment will become an "orphan" comment, unassociated with anything. + * + * If `nodeWithJavadoc` has no Javadoc comment, use the *first* + * orphan Javadoc comment in the *parent* scope, then *remove* that + * comment so that it doesn't "stick around" for the next member we + * attempt to grab Javadoc comments for. + */ + Node parent = node.getParentNode().get(); + for (Comment c : parent.getOrphanComments()) { + if (c.isJavadocComment()) { + javadoc = c.asJavadocComment(); + c.remove(); + break; + } + } + } + if (javadoc != null) { + member.setJavadocComment(javadoc.parse().toText()); + } + } + + private final void fillMethodBase(final JniMethodBaseInfo methodBaseInfo, final CallableDeclaration callableDecl) { + JniMethodInfo methodInfo = null; + if (methodBaseInfo instanceof JniMethodInfo) { + methodInfo = (JniMethodInfo) methodBaseInfo; + } + NodeWithParameters params = callableDecl; + for (final Parameter p : params.getParameters()) { + String name = p.getNameAsString(); + String javaType = getJavaType(methodBaseInfo.getDeclaringType(), methodInfo, p.getType()); + String jniType = getJniType(methodBaseInfo.getDeclaringType(), methodInfo, p.getType()); + methodBaseInfo.addParameter(new JniParameterInfo(name, javaType, jniType)); + } + } + + static String getJavaType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, Type type) { + String typeName = type.asString(); + if (methodInfo != null && methodInfo.getTypeParameters().contains(typeName)) + return typeName; + if (typeInfo.getTypeParameters().contains(typeName)) + return typeName; + try { + final ResolvedType rt = type.resolve(); + return rt.describe(); + } catch (final Throwable thr) { + return ".*" + type.asString(); + } + } + + static String getJniType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, Type type) { + if (type == null) { + return "Ljava/lang/Object;"; + } + + if (type.isArrayType()) { + return getJniType(typeInfo, methodInfo, type.asArrayType()); + } + if (type.isPrimitiveType()) { + return getPrimitiveJniType(type.asString()); + } + + if (methodInfo != null && methodInfo.getTypeParameters().contains(type.asString())) { + return methodInfo.getTypeParameterJniType(type.asString()); + } + if (typeInfo.getTypeParameters().contains(type.asString())) { + return typeInfo.getTypeParameterJniType(type.asString()); + } + + try { + return getJniType(type.resolve()); + } + catch (final Exception thr) { + } + return ".*" + type.asString(); + } + + static String getJniType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, ArrayType type) { + final int level = type.getArrayLevel(); + final StringBuilder depth = new StringBuilder(); + for (int i = 0; i < level; ++i) + depth.append("["); + return depth.toString() + getJniType(typeInfo, methodInfo, type.getElementType()); + } + + static String getPrimitiveJniType(String javaType) { + switch (javaType) { + case "boolean": return "Z"; + case "byte": return "B"; + case "char": return "C"; + case "double": return "D"; + case "float": return "F"; + case "int": return "I"; + case "long": return "J"; + case "short": return "S"; + case "void": return "V"; + } + throw new Error("Don't know JNI type for `" + javaType + "`!"); + } + + static String getJniType(ResolvedType type) { + if (type.isPrimitive()) { + return getPrimitiveJniType(type.asPrimitive().describe()); + } + if (type.isReferenceType()) { + return getJniType(type.asReferenceType()); + } + if (type.isVoid()) { + return "V"; + } + return "-" + type.getClass().getName() + "-"; + } + + static String getJniType(ResolvedReferenceType type) { + final Optional typeDeclOpt = type.getTypeDeclaration(); + if (!typeDeclOpt.isPresent()) + throw new Error("Can't get `ResolvedReferenceTypeDeclaration` for type `" + type.toString() + "`!"); + + final ResolvedReferenceTypeDeclaration typeDecl = typeDeclOpt.get(); + if (!type.hasName()) + throw new Error("Type `" + type.toString() + "` has no name!"); + + StringBuilder name = new StringBuilder(); + name.append("L"); + name.append(typeDecl.getPackageName()); + int len = name.length(); + for (int i = 0; i < len; ++i) { + if (name.charAt (i) == '.') { + name.setCharAt(i, '/'); + } + } + if (len > 1) { + name.append("/"); + } + name.append(typeDecl.getName().replace(".", "$")); + name.append(";"); + return name.toString(); + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java new file mode 100644 index 000000000..7dbc56f82 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java @@ -0,0 +1,103 @@ +package com.microsoft.android; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import com.microsoft.android.ast.*; +import com.microsoft.android.util.Parameter; + +public class ParameterNameGenerator implements AutoCloseable { + + final PrintStream output; + + public ParameterNameGenerator(final String output) throws FileNotFoundException, UnsupportedEncodingException { + if (output == null) + this.output = System.out; + else { + final File file = new File(output); + final File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + this.output = new PrintStream(file, "UTF-8"); + } + } + + public ParameterNameGenerator(final PrintStream output) { + Parameter.requireNotNull("output", output); + + this.output = output; + } + + public void close() { + if (output != System.out) { + output.flush(); + output.close(); + } + } + + public final void writePackages(final JniPackagesInfo packages) { + Parameter.requireNotNull("packages", packages); + + boolean first = true; + for (JniPackageInfo packageInfo : packages.getSortedPackages()) { + if (!first) + output.println(); + first = false; + writePackage(packageInfo); + } + } + + private final void writePackage(final JniPackageInfo packageInfo) { + if (packageInfo.getPackageName().length() > 0) { + output.println("package " + packageInfo.getPackageName()); + } + output.println(";---------------------------------------"); + + for (JniTypeInfo type : packageInfo.getSortedTypes()) { + writeType(type); + } + } + + private final void writeType(JniTypeInfo type) { + output.println(" " + type.getTypeKind() + " " + type.getName()); + final List sortedMethods = type.getSortedMembers() + .stream() + .filter(member -> member.isMethod() || member.isConstructor()) + .map(member -> (JniMethodBaseInfo) member) + .filter(method -> method.getParameters().size() > 0) + .collect(Collectors.toList()); + + for (JniMethodBaseInfo method : sortedMethods) { + output.print(" "); + if (method.isMethod()) { + JniMethodInfo m = (JniMethodInfo) method; + Collection typeParameters = m.getTypeParameters(); + if (typeParameters.size() > 0) { + output.print("<"); + output.print(String.join(", ", typeParameters)); + output.print("> "); + } + } + output.print(method.getName()); + output.print("("); + boolean first = true; + for (JniParameterInfo parameter : method.getParameters()) { + if (!first) { + output.print(", "); + } + first = false; + output.print(parameter.javaType); + output.print(" "); + output.print(parameter.name); + } + output.print(")"); + output.println(); + } + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java new file mode 100644 index 000000000..6253eaf28 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java @@ -0,0 +1,6 @@ +package com.microsoft.android.ast; + +public interface HasJavadocComment { + String getJavadocComment(); + void setJavadocComment(String javaDocComment); +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java new file mode 100644 index 000000000..6eec50deb --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java @@ -0,0 +1,12 @@ +package com.microsoft.android.ast; + +public class JniClassInfo extends JniTypeInfo { + public JniClassInfo(JniPackageInfo declaringPackage, String name) { + super(declaringPackage, name); + } + + @Override + public String getTypeKind() { + return "class"; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java new file mode 100644 index 000000000..adb099436 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java @@ -0,0 +1,17 @@ +package com.microsoft.android.ast; + +public final class JniConstructorInfo extends JniMethodBaseInfo { + public JniConstructorInfo(JniTypeInfo declaringType) { + super(declaringType, "#ctor"); + } + + @Override + public boolean isConstructor() { + return true; + } + + @Override + public String getJniSignature() { + return super.getJniSignature() + "V"; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java new file mode 100644 index 000000000..cfb87966e --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java @@ -0,0 +1,26 @@ +package com.microsoft.android.ast; + +import com.microsoft.android.util.Parameter; + +public final class JniFieldInfo extends JniMemberInfo { + + private String jniType; + + public JniFieldInfo(JniTypeInfo declaringType, String name) { + super(declaringType, name); + } + + @Override + public String getJniSignature() { + return jniType; + } + + @Override + public boolean isField() { + return true; + } + + public void setJniType(String jniType) { + this.jniType = Parameter.requireNotEmpty("jniType", jniType); + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java new file mode 100644 index 000000000..55d891765 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java @@ -0,0 +1,12 @@ +package com.microsoft.android.ast; + +public class JniInterfaceInfo extends JniTypeInfo { + public JniInterfaceInfo(JniPackageInfo declaringPackage, String name) { + super(declaringPackage, name); + } + + @Override + public String getTypeKind() { + return "interface"; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java new file mode 100644 index 000000000..7ee6005fb --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java @@ -0,0 +1,49 @@ +package com.microsoft.android.ast; + +import com.microsoft.android.util.Parameter; + +public abstract class JniMemberInfo implements HasJavadocComment { + private final String name; + private final JniTypeInfo declaringType; + + String javadocComment = ""; + + JniMemberInfo(final JniTypeInfo declaringType, String name) { + Parameter.requireNotNull("declaringType", declaringType); + + name = Parameter.requireNotEmpty("name", name); + + this.declaringType = declaringType; + this.name = name; + } + + public final JniTypeInfo getDeclaringType() { + return declaringType; + } + + public abstract String getJniSignature(); + + public final String getName() { + return name; + } + + public boolean isField() { + return false; + } + + public boolean isMethod() { + return false; + } + + public boolean isConstructor() { + return false; + } + + public final String getJavadocComment() { + return javadocComment; + } + + public final void setJavadocComment(String javaDocComment) { + this.javadocComment = Parameter.normalize(javaDocComment, ""); + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java new file mode 100644 index 000000000..4824c03c5 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java @@ -0,0 +1,36 @@ +package com.microsoft.android.ast; + +import java.util.ArrayList; +import java.util.Collection; + +import com.microsoft.android.util.Parameter; + +public abstract class JniMethodBaseInfo extends JniMemberInfo { + + private final Collection parameters = new ArrayList (); + + JniMethodBaseInfo(final JniTypeInfo declaringType, final String name) { + super(declaringType, name); + } + + public final void addParameter(final JniParameterInfo parameter) { + Parameter.requireNotNull("parameter", parameter); + + parameters.add(parameter); + } + + public Collection getParameters() { + return parameters; + } + + @Override + public String getJniSignature() { + final StringBuilder sig = new StringBuilder(); + sig.append("("); + for (JniParameterInfo p : parameters) { + sig.append(p.jniType); + } + sig.append(")"); + return sig.toString(); + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java new file mode 100644 index 000000000..b10074307 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java @@ -0,0 +1,64 @@ +package com.microsoft.android.ast; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + + +import com.microsoft.android.util.Parameter; + +public final class JniMethodInfo extends JniMethodBaseInfo { + + private String javaReturnType; + private String jniReturnType; + + private final Collection typeParameters = new ArrayList(); + private final Map jniTypes = new HashMap(); + + public JniMethodInfo(JniTypeInfo declaringType, String name) { + super(declaringType, name); + } + + @Override + public boolean isMethod() { + return true; + } + + public final void setReturnType(String javaType, String jniType) { + this.javaReturnType = Parameter.normalize(javaType, "void"); + this.jniReturnType = Parameter.normalize(jniType, "V"); + } + + public final void addTypeParameter(String typeParameter, String jniType) { + typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter); + jniType = Parameter.requireNotEmpty("jniType", jniType); + + if (typeParameters.contains(typeParameter)) + throw new IllegalArgumentException("Already added Type Parameter `" +typeParameter + "`"); + typeParameters.add(typeParameter); + jniTypes.put(typeParameter, jniType); + } + + public final Collection getTypeParameters() { + return typeParameters; + } + + public final String getTypeParameterJniType(String typeParameter) { + typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter); + return jniTypes.get(typeParameter); + } + + @Override + public String getJniSignature() { + return super.getJniSignature() + jniReturnType; + } + + public final String getJavaReturnType() { + return javaReturnType; + } + + public final String getJniReturnType() { + return jniReturnType; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java new file mode 100644 index 000000000..563426bf4 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java @@ -0,0 +1,47 @@ +package com.microsoft.android.ast; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.microsoft.android.util.Parameter; + +public final class JniPackageInfo { + private final String packageName; + + private final Map types = new HashMap(); + + public JniPackageInfo(String packageName) { + packageName = Parameter.normalize(packageName, ""); + + this.packageName = packageName; + } + + public final String getPackageName() { + return this.packageName; + } + + public final JniTypeInfo getType(String typeName) { + return types.getOrDefault(typeName, null); + } + + public final void add(JniTypeInfo type) { + if (types.containsKey(type.getName())) + throw new IllegalArgumentException("type"); + types.put(type.getName(), type); + } + + public final Collection getTypes() { + return types.values(); + } + + public final Collection getSortedTypes() { + final List sortedTypes = types.values() + .stream() + .sorted((t1, t2) -> t1.getRawName().compareTo(t2.getRawName())) + .collect(Collectors.toList()); + return sortedTypes; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java new file mode 100644 index 000000000..f7e84dcb0 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java @@ -0,0 +1,37 @@ +package com.microsoft.android.ast; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.microsoft.android.util.Parameter; + +public final class JniPackagesInfo { + + private final Map packages = new HashMap(); + + public JniPackageInfo getPackage(String packageName) { + packageName = Parameter.normalize(packageName, ""); + + if (!packages.containsKey(packageName)) { + JniPackageInfo newPackage = new JniPackageInfo(packageName); + packages.put(packageName, newPackage); + return newPackage; + } + return packages.get(packageName); + } + + public final Collection getPackages() { + return packages.values(); + } + + public final Collection getSortedPackages() { + final List sortedPackages = packages.values() + .stream() + .sorted((p1, p2) -> p1.getPackageName().compareTo(p2.getPackageName())) + .collect(Collectors.toList()); + return sortedPackages; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java new file mode 100644 index 000000000..dc6c177c4 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java @@ -0,0 +1,21 @@ +package com.microsoft.android.ast; + +public final class JniParameterInfo { + public final String name, jniType, javaType; + + public JniParameterInfo(String name, String javaType, String jniType) { + if (name == null || + (name = name.trim()).length() == 0) + throw new IllegalArgumentException("name"); + if (javaType == null || + (javaType = javaType.trim()).length() == 0) + throw new IllegalArgumentException("javaType"); + if (jniType == null || + (jniType = jniType.trim()).length() == 0) + throw new IllegalArgumentException("jniType"); + + this.name = name; + this.javaType = javaType; + this.jniType = jniType; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java new file mode 100644 index 000000000..cca84a953 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java @@ -0,0 +1,93 @@ +package com.microsoft.android.ast; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.microsoft.android.util.Parameter; + +public abstract class JniTypeInfo implements HasJavadocComment { + private final String name; + private final JniPackageInfo declaringPackage; + + private final Collection members = new ArrayList(); + private final Collection typeParameters = new ArrayList(); + private final Map jniTypes = new HashMap(); + + String javaDocComment = ""; + + JniTypeInfo(final JniPackageInfo declaringPackage, String name) { + Parameter.requireNotNull("declaringPackage", declaringPackage); + + name = Parameter.requireNotEmpty("name", name); + + this.declaringPackage = declaringPackage; + this.name = name; + } + + public abstract String getTypeKind(); + + public final JniPackageInfo getDeclaringPackage() { + return declaringPackage; + } + + public final String getName() { + if (typeParameters.isEmpty()) + return name; + return name + "<" + String.join(",", typeParameters) + ">"; + } + + public final String getRawName() { + return name; + } + + public final void addTypeParameter(String typeParameter, String jniType) { + typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter); + jniType = Parameter.requireNotEmpty("jniType", jniType); + + if (typeParameters.contains(typeParameter)) { + jniTypes.replace(typeParameter, jniType); + return; + } + typeParameters.add(typeParameter); + jniTypes.put(typeParameter, jniType); + } + + public final Collection getTypeParameters() { + return typeParameters; + } + + public final String getTypeParameterJniType(String typeParameter) { + typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter); + return jniTypes.get(typeParameter); + } + + public final void add(JniMemberInfo member) { + Parameter.requireNotNull("member", member); + + members.add(member); + } + + public final Collection getMembers() { + return members; + } + + public final String getJavadocComment() { + return javaDocComment; + } + + public final void setJavadocComment(String javaDocComment) { + this.javaDocComment = Parameter.normalize(javaDocComment, ""); + } + + public final Collection getSortedMembers() { + final List sortedMembers = members + .stream() + .sorted((m1, m2) -> (m1.getName() + "." + m1.getJniSignature()).compareTo((m2.getName() + "." + m2.getJniSignature()))) + .collect(Collectors.toList()); + return sortedMembers; + } +} diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java b/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java new file mode 100644 index 000000000..6b0ec1b39 --- /dev/null +++ b/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java @@ -0,0 +1,27 @@ +package com.microsoft.android.util; + +public final class Parameter { + private Parameter() { + } + + public static String requireNotEmpty(String parameterName, String value) { + if (value == null || + (value = value.trim()).length() == 0) + throw new IllegalArgumentException(parameterName); + return value; + } + + public static T requireNotNull(String parameterName, T value) { + if (value == null) + throw new IllegalArgumentException(parameterName); + return value; + } + + + public static String normalize(String value, String defaultValue) { + if (value == null || + (value = value.trim()).length() == 0) + value = defaultValue; + return value; + } +} diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java new file mode 100644 index 000000000..8484cbe74 --- /dev/null +++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java @@ -0,0 +1,20 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package com.microsoft.android; + +import java.io.IOException; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class JavaSourceUtilsOptionsTest { + + @Test public void testParse_HelpOptionReturnsNull() throws IOException { + JavaSourceUtilsOptions options; + options = JavaSourceUtilsOptions.parse(new String[]{"--help"}); + assertNull(options); + options = JavaSourceUtilsOptions.parse(new String[]{"-h"}); + assertNull(options); + } +} diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java new file mode 100644 index 000000000..e16c5d26c --- /dev/null +++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java @@ -0,0 +1,125 @@ +package com.microsoft.android; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.junit.Test; +import static org.junit.Assert.*; + +import com.github.javaparser.JavaParser; +import com.microsoft.android.ast.*; + +public final class JavadocXmlGeneratorTest { + @Test(expected = FileNotFoundException.class) + public void init_invalidFileThrows() throws FileNotFoundException, UnsupportedEncodingException { + try (JavadocXmlGenerator g = new JavadocXmlGenerator("/this/file/does/not/exist")) { + } + } + + @Test(expected = IllegalArgumentException.class) + public void testWritePackages_nullPackages() throws ParserConfigurationException, TransformerException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes)); + + generator.writePackages(null); + } + + @Test + public void testWritePackages_noPackages() throws ParserConfigurationException, TransformerException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes)); + + JniPackagesInfo packages = new JniPackagesInfo(); + generator.writePackages(packages); + + final String expected = + "\n" + + "\n"; + assertEquals("no packages", expected, bytes.toString()); + } + + + @Test + public void testWritePackages_demo() throws ParserConfigurationException, TransformerException { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes)); + final JniPackagesInfo packages = JniPackagesInfoTest.createDemoInfo(); + + final String expected = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " .(ILjava/lang/String;)V]]>\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + generator.writePackages(packages); + assertEquals("global package + example packages", expected, bytes.toString()); + } + + @Test + public void testWritePackages_Outer_java() throws Throwable { + testWritePackages("Outer.java", "Outer.xml"); + } + + @Test + public void testWritePackages_JavaType_java() throws Throwable { + testWritePackages("../../../com/xamarin/JavaType.java", "JavaType.xml"); + } + + private static void testWritePackages(final String resourceJava, final String resourceXml) throws Throwable { + final JavaParser parser = JniPackagesInfoFactoryTest.createParser(); + final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser); + final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource(resourceJava).toURI()); + final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource})); + + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes)); + + final String expected = JniPackagesInfoTest.getResourceContents(resourceXml); + + generator.writePackages(packagesInfo); + assertEquals(resourceJava + " Javadoc XML", expected, bytes.toString()); + } +} diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java new file mode 100644 index 000000000..730f14baa --- /dev/null +++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java @@ -0,0 +1,59 @@ +package com.microsoft.android; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.util.Arrays; +import java.io.File; + +import org.junit.Test; +import static org.junit.Assert.*; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.symbolsolver.*; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.*; + + +import com.microsoft.android.ast.*; + +public class JniPackagesInfoFactoryTest { + + @Test(expected = IllegalArgumentException.class) + public void testInit_nullParser() { + new JniPackagesInfoFactory(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testParse_nullFiles() throws Throwable { + final JavaParser parser = new JavaParser(); + final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser); + factory.parse(null); + } + + @Test + public void testParse_demo() throws Throwable { + final JavaParser parser = createParser(); + final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser); + final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource("Outer.java").toURI()); + final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource})); + + assertEquals("Only one package processed", 1, packagesInfo.getPackages().size()); + final JniPackageInfo p = packagesInfo.getPackage("example"); + assertNotNull("Should have found `example` package", p); + assertEquals("Outer & Outer.Inner & Outer.Inner.NestedInner & Outer.MyAnnotation types found", 4, p.getTypes().size()); + + JniTypeInfo info = p.getType("Outer"); + assertNotNull(info); + assertEquals("Outer", info.getName()); + } + + static JavaParser createParser() { + final CombinedTypeSolver typeSolver = new CombinedTypeSolver(); + typeSolver.add(new ReflectionTypeSolver()); + final ParserConfiguration config = new ParserConfiguration(); + config.setSymbolResolver(new JavaSymbolSolver(typeSolver)); + return new JavaParser(config); + } +} diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java new file mode 100644 index 000000000..845bd091c --- /dev/null +++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java @@ -0,0 +1,82 @@ +package com.microsoft.android; + +import java.io.*; +import java.net.URISyntaxException; + +import com.microsoft.android.ast.*; + +public class JniPackagesInfoTest { + + static JniPackagesInfo createDemoInfo() { + JniPackagesInfo packages = new JniPackagesInfo(); + JniPackageInfo global = packages.getPackage(null); + + JniTypeInfo type = new JniClassInfo(global, "A"); + type.setJavadocComment("jni-sig=LA;"); + global.add(type); + + JniFieldInfo field = new JniFieldInfo(type, "field"); + field.setJniType("I"); + field.setJavadocComment("jni-sig=field.I"); + type.add(field); + + JniConstructorInfo init = new JniConstructorInfo(type); + init.addParameter(new JniParameterInfo("one", "int", "I")); + init.addParameter(new JniParameterInfo("two", "java.lang.String", "Ljava/lang/String;")); + init.setJavadocComment("jni-sig=.(ILjava/lang/String;)V"); + type.add(init); + + JniMethodInfo method = new JniMethodInfo(type, "m"); + method.addTypeParameter("T", "Ljava/lang/Object;"); + method.addParameter(new JniParameterInfo("value", "T", "Ljava/lang/Object;")); + method.addParameter(new JniParameterInfo("x", "long", "J")); + method.setReturnType("void", "V"); + method.setJavadocComment("jni-sig=m.(Ljava/lang/Object;J)V"); + type.add(method); + + type = new JniInterfaceInfo(global, "I"); + type.addTypeParameter("T", "Ljava/lang/Object;"); + type.setJavadocComment("jni-sig=LI;"); + global.add(type); + method = new JniMethodInfo(type, "m"); + method.addParameter(new JniParameterInfo("x", "java.util.List", "Ljava/util/List;")); + method.setReturnType("T", "Ljava/lang/Object;"); + method.setJavadocComment("jni-sig=m.(Ljava/util/List;)Ljava/lang/Object;"); + type.add(method); + + JniPackageInfo example = packages.getPackage("example"); + type = new JniInterfaceInfo(example, "Exampleable"); + type.setJavadocComment("jni-sig=Lexample/Exampleable;"); + example.add(type); + + method = new JniMethodInfo(type, "noParameters"); + method.setReturnType("void", "V"); + method.setJavadocComment("jni-sig=noParameters.()V"); + type.add(method); + + method = new JniMethodInfo(type, "example"); + method.addParameter(new JniParameterInfo("e", "java.lang.String", "Ljava/lang/String;")); + method.setReturnType("void", "V"); + method.setJavadocComment("jni-sig=example.(Ljava/lang/String;)V"); + type.add(method); + + packages.getPackage("before.example"); + + return packages; + } + + static String getResourceContents(String resourceName) throws IOException, URISyntaxException { + final File resourceFile = new File(JniPackagesInfoTest.class.getResource(resourceName).toURI()); + final StringBuilder contents = new StringBuilder(); + final String lineEnding = System.getProperty("line.separator"); + + String line; + try (final BufferedReader reader = new BufferedReader(new FileReader (resourceFile))) { + while((line = reader.readLine()) != null) { + contents.append(line); + contents.append(lineEnding); + } + } + return contents.toString(); + } +} diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java new file mode 100644 index 000000000..99b7ea4ca --- /dev/null +++ b/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java @@ -0,0 +1,93 @@ +package com.microsoft.android; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import org.junit.Test; +import static org.junit.Assert.*; + +import com.github.javaparser.JavaParser; +import com.microsoft.android.ast.*; + +public class ParameterNameGeneratorTest { + + @Test(expected = FileNotFoundException.class) + public void init_invalidFileThrows() throws FileNotFoundException, UnsupportedEncodingException { + new ParameterNameGenerator("/this/file/does/not/exist"); + } + + @Test(expected = IllegalArgumentException.class) + public void testWritePackages_nullPackages() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes)); + + generator.writePackages(null); + } + + @Test + public void testWritePackages_noPackages() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes)); + + JniPackagesInfo packages = new JniPackagesInfo(); + generator.writePackages(packages); + assertEquals("no packages", "", bytes.toString()); + } + + + @Test + public void testWritePackages_demo() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes)); + JniPackagesInfo packages = JniPackagesInfoTest.createDemoInfo(); + + final String expected = + ";---------------------------------------\n" + + " class A\n" + + " #ctor(int one, java.lang.String two)\n" + + " m(T value, long x)\n" + + " interface I\n" + + " m(java.util.List x)\n" + + "\n" + + "package before.example\n" + + ";---------------------------------------\n" + + "\n" + + "package example\n" + + ";---------------------------------------\n" + + " interface Exampleable\n" + + " example(java.lang.String e)\n" + + ""; + + generator.writePackages(packages); + assertEquals("global package + example packages", expected, bytes.toString()); + } + + @Test + public void testWritePackages_Outer_java() throws Throwable { + testWritePackages("Outer.java", "Outer.params.txt"); + } + + @Test + public void testWritePackages_JavaType_java() throws Throwable { + testWritePackages("../../../com/xamarin/JavaType.java", "JavaType.params.txt"); + } + + private static void testWritePackages(final String resourceJava, final String resourceParamsTxt) throws Throwable { + final JavaParser parser = JniPackagesInfoFactoryTest.createParser(); + final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser); + final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource(resourceJava).toURI()); + final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource})); + + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes)); + + final String expected = JniPackagesInfoTest.getResourceContents(resourceParamsTxt); + + generator.writePackages(packagesInfo); + assertEquals(resourceJava + " parameter names", expected, bytes.toString()); + } +} diff --git a/tools/java-source-utils/src/test/resources/com/.gitignore b/tools/java-source-utils/src/test/resources/com/.gitignore new file mode 100644 index 000000000..f815dd72b --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/.gitignore @@ -0,0 +1 @@ +xamarin diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore b/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore new file mode 100644 index 000000000..6b468b62a --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore @@ -0,0 +1 @@ +*.class diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt new file mode 100644 index 000000000..e49e266d0 --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt @@ -0,0 +1,21 @@ +package com.xamarin +;--------------------------------------- + class JavaEnum + class JavaType + #ctor(java.lang.String value) + action(java.lang.Object value) + compareTo(com.xamarin.JavaType value) + finalize(int value) + func(java.lang.StringBuilder value) + func(java.lang.String[] values) + instanceActionWithGenerics(T value1, E value2) + staticActionWithGenerics(T value1, TExtendsNumber value2, java.util.List unboundedList, java.util.List extendsList, java.util.List superList) + sum(int first, int remaining) + class JavaType.ASC + class JavaType.PSC + class JavaType.RNC + #ctor(E value1, E2 value2) + fromE(E value) + class JavaType.RNC.RPNC + #ctor(E value1, E2 value2, E3 value3) + fromE2(E2 value) diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml new file mode 100644 index 000000000..ff472f1c1 --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml @@ -0,0 +1,174 @@ + + + + + + + Paragraphs of text? + +@return some value]]> + + + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java new file mode 100644 index 000000000..3087e5d28 --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java @@ -0,0 +1,121 @@ +package example; + +import java.util.List; +import java.util.Map; + + +/** + * Yay, Javadoc! + * + * JNI sig: Lexample/Outer; + */ + + @Outer.MyAnnotation(keys={"a", "b", "c"}) +public class Outer { + + /** + * (java.lang.Object value) + * + * JNI sig: (Ljava/lang/Object;)V + */ + + public Outer(T value) { + value.run(); + } + + /** + * isU(java.util.List list) + * + *

This is a paragraph. Yay?

+ * + * JNI sig: (Ljava/util/List;)Ljava/lang/Error; + * + * @param list just some random items + * @return some value + */ + + public U isU(List list) { + return null; + } + + /** + * Just an example annotation, for use later… + * + * JNI sig: Lexample/Outer$MyAnnotation; + */ + + @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE}) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) + public static @interface MyAnnotation { + + /** + * JNI sig: ()[Ljava/lang/String; + * + * @return some random keys + */ + + String[] keys() default {}; + } + + /** + * JNI sig: Lexample/Outer$Inner; + */ + public static interface Inner { + /** + * m(U value) + * + * JNI sig: ([[Ljava/lang/Readable;)V + * + * @throws Throwable never, just because + */ + public default void m(V[][] values) throws Throwable { + for (V[] vs : values) { + for (V v : vs) { + v.read(null); + } + } + } + + /** + * JNI sig: J + */ + public static final long COUNT = 42; + + /** + * JNI sig: Lexample/Outer$Inner$NestedInner; + */ + public static class NestedInner { + + /** + * JNI sig: S + */ + public static final short S = 64; + + /** + * map(java.util.Map map) + * + * JNI sig: map(Ljava/util/Map;)V + * @param map + */ + public void map(Map m) { + } + } + } + + /** + * method(java.lang.CharSequence a, short[] b, T[] values) + * + * JNI sig: (Ljava/lang/CharSequence;[S[Ljava/lang/Appendable;)Ljava/lang/Appendable; + */ + public T method(CharSequence a, short[] b, T[] values) { + return null; + } + + /** + * main(java.lang.String[] args) + * + * JNI sig: ([Ljava/lang/String;)V + */ + public static void main(String[] args) { + } +} diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt new file mode 100644 index 000000000..fc2a52c46 --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt @@ -0,0 +1,12 @@ +package example +;--------------------------------------- + class Outer + #ctor(T value) + isU(java.util.List list) + main(java.lang.String[] args) + method(java.lang.CharSequence a, short[] b, T[] values) + interface Outer.Inner + m(V[][] values) + class Outer.Inner.NestedInner + map(java.util.Map m) + interface Outer.MyAnnotation diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml new file mode 100644 index 000000000..e073732d1 --- /dev/null +++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml @@ -0,0 +1,79 @@ + + + + + + + + (java.lang.Object value) + +JNI sig: (Ljava/lang/Object;)V]]> + + + + list) + +

This is a paragraph. Yay?

+ +JNI sig: (Ljava/util/List;)Ljava/lang/Error; + +@param list just some random items +@return some value]]>
+
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + map) + +JNI sig: map(Ljava/util/Map;)V + +@param map]]> + + + + + + + + +
+
From cee99741c6aa513fe175efc9daf9d163d4f7b3ea Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 30 Jul 2020 16:04:49 -0400 Subject: [PATCH 2/3] [class-parse] Merge javadoc documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/xamarin/java.interop/pull/623 Context: https://github.com/xamarin/java.interop/pull/623#issuecomment-666554407 DO NOT MERGE UNTIL AFTER PR #623 IS MERGED. Update `class-parse --docspath=PATH` so that if `PATH` contains `` elements, as produced by `tools/java-source-utils` (PR #623), then those `` elements will be inserted into the generated API description. The intent is to eventually allow `generator` to emit the `` data as C# XML Documentation, allowing a pipeline of: java -jar java-source-tools path/to/android.jar --output-javadoc android.xml mono class-parse.exe --docspath=android.xml -o api.xml … mono generator.exe api.xml … --- .../ClassPath.cs | 49 ++++++++++++++++-- .../JavaDocumentScraper.cs | 6 +-- .../XmlClassDeclarationBuilder.cs | 47 ++++++++++++++++- .../ParameterFixupTests.cs | 15 ++++++ .../ParameterFixupApiXmlJavadocs.xml | 12 +++++ .../Resources/ParameterFixupFromJavadocs.xml | 50 +++++++++++++++++++ 6 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml create mode 100644 tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml diff --git a/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs b/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs index 62adb9a5d..f5a54d54b 100644 --- a/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs +++ b/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs @@ -27,7 +27,29 @@ public class ClassPath { public string ApiSource { get; set; } - public IEnumerable DocumentationPaths { get; set; } + IEnumerable docPaths; + Dictionary xmlDocPaths; + + public IEnumerable DocumentationPaths { + get {return docPaths;} + set { + if (xmlDocPaths != null) + xmlDocPaths = null; + this.docPaths = value; + if (this.docPaths == null) { + return; + } + foreach (var path in docPaths) { + if (path == null) + continue; + if (JavaMethodParameterNameProvider.GetDocletType (path) != JavaDocletType._ApiXml) + continue; + if (xmlDocPaths == null) + xmlDocPaths = new Dictionary (); + xmlDocPaths [path] = XDocument.Load (path); + } + } + } public string AndroidFrameworkPlatform { get; set; } @@ -244,13 +266,15 @@ void FixupParametersFromDocs (XElement api) IJavaMethodParameterNameProvider CreateDocScraper (string src) { + if (xmlDocPaths != null && xmlDocPaths.TryGetValue (src, out var doc)) { + return new ApiXmlDocScraper (doc); + } switch (JavaMethodParameterNameProvider.GetDocletType (src)) { default: return new DroidDoc2Scraper (src); case JavaDocletType.DroidDoc: return new DroidDocScraper (src); case JavaDocletType.Java6: return new JavaDocScraper (src); case JavaDocletType.Java7: return new Java7DocScraper (src); case JavaDocletType.Java8: return new Java8DocScraper (src); - case JavaDocletType._ApiXml: return new ApiXmlDocScraper (src); case JavaDocletType.JavaApiParameterNamesXml: return new JavaParameterNamesLoader (src); } } @@ -310,11 +334,30 @@ public XElement ToXElement () new XAttribute ("name", p), new XAttribute ("jni-name", p.Replace ('.', '/')), packagesDictionary [p].OrderBy (c => c.ThisClass.Name.Value, StringComparer.OrdinalIgnoreCase) - .Select (c => new XmlClassDeclarationBuilder (c).ToXElement ())))); + .Select (c => new XmlClassDeclarationBuilder (c, GetJavadocsElement (c)).ToXElement ())))); FixupParametersFromDocs (api); return api; } + XElement GetJavadocsElement (ClassFile type) + { + if (xmlDocPaths == null) + return null; + foreach (var path in docPaths) { + if (!xmlDocPaths.TryGetValue (path, out var doc)) + continue; + var typeXml = doc.Elements ("api") + .Elements ("package") + .Where (p => type.PackageName == (string) p.Attribute ("name")) + .Elements () + .Where (e => type.FullJniName == (string) e.Attribute ("jni-signature")) + .FirstOrDefault (); + if (typeXml != null) + return typeXml; + } + return null; + } + public void SaveXmlDescription (string fileName) { var encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false); diff --git a/src/Xamarin.Android.Tools.Bytecode/JavaDocumentScraper.cs b/src/Xamarin.Android.Tools.Bytecode/JavaDocumentScraper.cs index 3adb738ef..f0e55b8b3 100644 --- a/src/Xamarin.Android.Tools.Bytecode/JavaDocumentScraper.cs +++ b/src/Xamarin.Android.Tools.Bytecode/JavaDocumentScraper.cs @@ -328,7 +328,7 @@ public static JavaDocletType GetDocletType (string path) int len = reader.ReadBlock (buf, 0, buf.Length); rawXML = new string (buf, 0, len).Trim (); } - if (rawXML.Contains ("") && rawXML.Contains ("") || rawXML.Contains (" element == "constructor" + ? descriptor == (string) e.Attribute ("jni-signature") + : name == (string) e.Attribute ("name") && descriptor == (string) e.Attribute ("jni-signature")) + .Elements ("javadoc") + .FirstOrDefault (); + return r; } static XAttribute GetNative (MethodInfo method) @@ -500,10 +531,22 @@ IEnumerable GetFields () GetNotNull (field), GetValue (field), new XAttribute ("visibility", visibility), - new XAttribute ("volatile", (field.AccessFlags & FieldAccessFlags.Volatile) != 0)); + new XAttribute ("volatile", (field.AccessFlags & FieldAccessFlags.Volatile) != 0), + GetFieldJavadoc (field.Name)); } } + XElement GetFieldJavadoc (string fieldName) + { + if (javadocsSource == null) + return null; + return javadocsSource + .Elements ("field") + .Where (f => fieldName == (string) f.Attribute ("name")) + .Elements ("javadoc") + .FirstOrDefault (); + } + string GetGenericType (FieldInfo field) { var signature = field.GetSignature (); diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs b/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs index 2c2191d72..8b5527587 100644 --- a/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs @@ -30,6 +30,21 @@ public void XmlDeclaration_FixedUpFromDocumentation() } } + [Test] + public void XmlDeclaration_FixedUpFromApiXmlJavadocs () + { + string tempFile = null; + + try { + tempFile = LoadToTempFile ("ParameterFixupApiXmlJavadocs.xml"); + + AssertXmlDeclaration ("Collection.class", "ParameterFixupFromJavadocs.xml", tempFile); + } finally { + if (File.Exists (tempFile)) + File.Delete (tempFile); + } + } + [Test] public void XmlDeclaration_FixedUpFromApiXmlDocumentation () { diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml new file mode 100644 index 000000000..eb42329d8 --- /dev/null +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml new file mode 100644 index 000000000..63f7cf639 --- /dev/null +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + From 671b0d738044fa076d696681670f8c9367006eb5 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 31 Jul 2020 12:10:13 -0400 Subject: [PATCH 3/3] [generator] Convert Javadoc to C# XML docs --- .../JavaApi.XmlModel.cs | 2 ++ .../JavaApiXmlGeneratorExtensions.cs | 12 +++++++++++ .../JavaApiXmlLoaderExtensions.cs | 21 +++++++++++++++++-- .../CodeGenerator.cs | 7 +++++++ .../XmlApiImporter.cs | 15 +++++++++---- .../Field.cs | 2 ++ .../GenBase.cs | 2 ++ .../MethodBase.cs | 2 ++ 8 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApi.XmlModel.cs b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApi.XmlModel.cs index cf9c8cde7..5581f1c32 100644 --- a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApi.XmlModel.cs +++ b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApi.XmlModel.cs @@ -59,6 +59,7 @@ protected JavaType (JavaPackage parent) public string Name { get; set; } public bool Static { get; set; } public string Visibility { get; set; } + public string Javadoc { get; set; } public string ExtendedJniSignature { get; set; } @@ -168,6 +169,7 @@ protected JavaMember (JavaType parent) public bool Static { get; set; } public string Visibility { get; set; } public string ExtendedJniSignature { get; set; } + public string Javadoc { get; set; } } public partial class JavaField : JavaMember diff --git a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlGeneratorExtensions.cs b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlGeneratorExtensions.cs index 83e44f7db..88c8e2059 100644 --- a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlGeneratorExtensions.cs +++ b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlGeneratorExtensions.cs @@ -89,6 +89,12 @@ static void SaveTypeCommon (this JavaType cls, XmlWriter writer, string elementN if (cls.TypeParameters != null) cls.TypeParameters.Save (writer, " "); + if (cls.Javadoc != null) { + writer.WriteStartElement ("javadoc"); + writer.WriteCData (cls.Javadoc); + writer.WriteFullEndElement (); + } + foreach (var m in cls.Members.OfType ().OrderBy (m => m.Name, StringComparer.Ordinal).ThenBy (m => string.Join (", ", m.Parameters.Select (p => p.Type))).ThenBy (m => m.ExtendedSynthetic)) m.Save (writer); foreach (var m in cls.Members.OfType ().OrderBy (m => m.Name, StringComparer.Ordinal).ThenBy (m => string.Join (", ", m.Parameters.Select (p => p.Type))).ThenBy (m => m.ExtendedSynthetic)) @@ -288,6 +294,12 @@ static void SaveCommon (this JavaMember m, XmlWriter writer, string elementName, } } + if (m.Javadoc != null) { + writer.WriteStartElement ("javadoc"); + writer.WriteCData (m.Javadoc); + writer.WriteFullEndElement (); + } + if (exceptions != null) { foreach (var e in exceptions.OrderBy (e => e.Name.Substring (e.Name.LastIndexOf ('/') + 1).Replace ('$', '.'), StringComparer.Ordinal)) { writer.WriteStartElement ("exception"); diff --git a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlLoaderExtensions.cs b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlLoaderExtensions.cs index 218488f3c..bc73974cb 100644 --- a/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlLoaderExtensions.cs +++ b/src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlLoaderExtensions.cs @@ -117,6 +117,8 @@ internal static bool TryLoadCommonElement (this JavaType type, XmlReader reader) var tp = new JavaTypeParameters (type); tp.Load (reader); type.TypeParameters = tp; + } else if (reader.LocalName == "javadoc") { + type.Javadoc = LoadJavadoc (reader); } else if (reader.LocalName == "field") { var field = new JavaField (type); field.Load (reader); @@ -129,6 +131,13 @@ internal static bool TryLoadCommonElement (this JavaType type, XmlReader reader) return false; return true; } + + static string LoadJavadoc (XmlReader reader) + { + var javadoc = reader.ReadElementContentAsString (); + reader.Skip (); + return javadoc; + } public static void Load (this JavaInterface iface, XmlReader reader) { @@ -172,14 +181,14 @@ public static void Load (this JavaClass kls, XmlReader reader) if (reader.NodeType == XmlNodeType.EndElement) break; // if (reader.NodeType != XmlNodeType.Element) - throw XmlUtil.UnexpectedElementOrContent ("class", reader, "implements", "typeParameters", "field", "constructor", "method"); + throw XmlUtil.UnexpectedElementOrContent ("class", reader, "implements", "typeParameters", "javadoc", "field", "constructor", "method"); if (!kls.TryLoadCommonElement (reader)) { if (reader.LocalName == "constructor") { var constructor = new JavaConstructor (kls); constructor.Load (reader); kls.Members.Add (constructor); } else - throw XmlUtil.UnexpectedElementOrContent ("class", reader, "implements", "typeParameters", "field", "constructor", "method"); + throw XmlUtil.UnexpectedElementOrContent ("class", reader, "implements", "typeParameters", "javadoc", "field", "constructor", "method"); } } while (true); XmlUtil.VerifyEndElement (reader, "class"); @@ -221,6 +230,12 @@ public static void Load (this JavaField field, XmlReader reader) field.Value = reader.GetAttribute ("value"); field.NotNull = reader.GetAttribute ("not-null") == "true"; + if (!reader.IsEmptyElement) { + reader.Read (); + reader.MoveToContent (); + field.Javadoc = LoadJavadoc (reader); + } + reader.Skip (); } @@ -252,6 +267,8 @@ static void LoadMethodBase (this JavaMethodBase methodBase, string elementName, var p = new JavaParameter (methodBase); p.Load (reader); methodBase.Parameters.Add (p); + } else if (reader.LocalName == "javadoc") { + methodBase.Javadoc = LoadJavadoc (reader); } else if (reader.LocalName == "exception") { var p = new JavaException (); p.Load (reader); diff --git a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs index b1b3a4745..41b9412ea 100644 --- a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs +++ b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs @@ -67,6 +67,8 @@ public void WriteClass (ClassGen @class, string indent, GenerationInfo gen_info) obj_type = gs != null && gs.IsConcrete ? gs.GetGenericType (null) : opt.GetOutputName (@class.base_symbol.FullName); } + Javadoc.WriteJavadocs (writer, indent, @class.Javadoc); + writer.WriteLine ("{0}// Metadata.xml XPath class reference: path=\"{1}\"", indent, @class.MetadataXPathReference); if (@class.IsDeprecated) @@ -412,6 +414,8 @@ public bool WriteFields (List fields, string indent, GenBase gen, HashSet internal virtual void WriteField (Field field, string indent, GenBase type) { + Javadoc.WriteJavadocs (writer, indent, field.Javadoc); + if (field.IsEnumified) writer.WriteLine ("[global::Android.Runtime.GeneratedEnum]"); if (field.NeedsProperty) { @@ -520,6 +524,8 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent, G sb.Append (opt.GetOutputName (isym.FullName)); } + Javadoc.WriteJavadocs (writer, indent, @interface.Javadoc); + writer.WriteLine ("{0}// Metadata.xml XPath interface reference: path=\"{1}\"", indent, @interface.MetadataXPathReference); if (@interface.IsDeprecated) @@ -1436,6 +1442,7 @@ public void WriteMethod (Method method, string indent, GenBase type, bool genera } string ret = opt.GetTypeReferenceName (method.RetVal); WriteMethodIdField (method, indent); + Javadoc.WriteJavadocs (writer, indent, method.Javadoc); if (method.DeclaringType.IsGeneratable) writer.WriteLine ("{0}// Metadata.xml XPath method reference: path=\"{1}\"", indent, method.GetMetadataXPathReference (method.DeclaringType)); if (method.Deprecated != null) diff --git a/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs b/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs index f35afe4c5..f5ad54612 100644 --- a/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs +++ b/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs @@ -19,6 +19,7 @@ public static ClassGen CreateClass (XElement pkg, XElement elem, CodeGenerationO FromXml = true, IsAbstract = elem.XGetAttribute ("abstract") == "true", IsFinal = elem.XGetAttribute ("final") == "true", + Javadoc = elem.Element ("javadoc")?.Value, // Only use an explicitly set XML attribute Unnest = elem.XGetAttribute ("unnest") == "true" ? true : elem.XGetAttribute ("unnest") == "false" ? false : @@ -43,8 +44,9 @@ public static ClassGen CreateClass (XElement pkg, XElement elem, CodeGenerationO case "field": klass.AddField (CreateField (klass, child)); break; - case "typeParameters": - break; // handled at GenBaseSupport + case "javadoc": // Handled in `new ClassGen() {…}` + case "typeParameters": // handled at GenBaseSupport + break; default: Report.Warning (0, Report.WarningClassGen + 1, "unexpected class child {0}.", child.Name); break; @@ -61,6 +63,7 @@ public static Ctor CreateCtor (GenBase declaringType, XElement elem) CustomAttributes = elem.XGetAttribute ("customAttributes"), Deprecated = elem.Deprecated (), GenericArguments = elem.GenericArguments (), + Javadoc = elem.Element ("javadoc")?.Value, Name = elem.XGetAttribute ("name"), Visibility = elem.Visibility () }; @@ -108,6 +111,7 @@ public static Field CreateField (GenBase declaringType, XElement elem) IsDeprecatedError = elem.XGetAttribute ("deprecated-error") == "true", IsFinal = elem.XGetAttribute ("final") == "true", IsStatic = elem.XGetAttribute ("static") == "true", + Javadoc = elem.Element ("javadoc")?.Value, JavaName = elem.XGetAttribute ("name"), NotNull = elem.XGetAttribute ("not-null") == "true", SetterParameter = CreateParameter (elem), @@ -206,6 +210,7 @@ public static InterfaceGen CreateInterface (XElement pkg, XElement elem, CodeGen var iface = new InterfaceGen (CreateGenBaseSupport (pkg, elem, true)) { ArgsType = elem.XGetAttribute ("argsType"), HasManagedName = elem.Attribute ("managedName") != null, + Javadoc = elem.Element ("javadoc")?.Value, NoAlternatives = elem.XGetAttribute ("no-alternatives") == "true", // Only use an explicitly set XML attribute Unnest = elem.XGetAttribute ("unnest") == "true" ? true : @@ -228,8 +233,9 @@ public static InterfaceGen CreateInterface (XElement pkg, XElement elem, CodeGen case "field": iface.AddField (CreateField (iface, child)); break; - case "typeParameters": - break; // handled at GenBaseSupport + case "javadoc": // Handled in `new InterfaceGen() {…}` + case "typeParameters": // handled at GenBaseSupport + break; default: Report.Warning (0, Report.WarningInterfaceGen + 0, "unexpected interface child {0}.", child); break; @@ -255,6 +261,7 @@ public static Method CreateMethod (GenBase declaringType, XElement elem) IsFinal = elem.XGetAttribute ("final") == "true", IsReturnEnumified = elem.Attribute ("enumReturn") != null, IsStatic = elem.XGetAttribute ("static") == "true", + Javadoc = elem.Element ("javadoc")?.Value, JavaName = elem.XGetAttribute ("name"), ManagedReturn = elem.XGetAttribute ("managedReturn"), PropertyNameOverride = elem.XGetAttribute ("propertyName"), diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs index 4d599f4f5..c436df0f1 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs @@ -25,6 +25,8 @@ public class Field : ApiVersionsSupport.IApiAvailability public string Value { get; set; } public string Visibility { get; set; } + public string Javadoc { get; set; } + internal string GetMethodPrefix => TypeNameUtilities.GetCallPrefix (Symbol); internal string ID => JavaName + "_jfieldId"; diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs index c971ede66..fd3abd22b 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs @@ -35,6 +35,8 @@ protected GenBase (GenBaseSupport support) public string ReturnCast => string.Empty; + public string Javadoc { get; set; } + // This means Ctors/Methods/Properties/Fields has not been populated yet. // If this type is retrieved from the SymbolTable, it will call PopulateAction // to fill in members before returning it to the user. diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/MethodBase.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/MethodBase.cs index c0b3d5efe..0f89d48fe 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/MethodBase.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/MethodBase.cs @@ -24,6 +24,8 @@ protected MethodBase (GenBase declaringType) public ParameterList Parameters { get; } = new ParameterList (); public string Visibility { get; set; } + public string Javadoc { get; set; } + public string [] AutoDetectEnumifiedOverrideParameters (AncestorDescendantCache cache) { if (Parameters.All (p => p.Type != "int"))