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 000000000..87b738cbd Binary files /dev/null and b/build-tools/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..6b3851a8a --- /dev/null +++ b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/build-tools/gradle/gradlew b/build-tools/gradle/gradlew new file mode 100755 index 000000000..af6708ff2 --- /dev/null +++ b/build-tools/gradle/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-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/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 @@ + + + + + + + + + + + + + + + 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]]> + + + + + + + + +
+