From a3b74568195310012ffaa9289600b711cf24d262 Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Fri, 28 Jun 2019 13:03:39 -0500 Subject: [PATCH] [generator] Add support for const fields on interfaces (#439) Add support for Java-side interface constants to be optionally bound using C# 8.0's new default interface member support. This is feature-flagged behind the `--lang-features=interface-constants` option (off by default): generator --lang-features=interface-constants ... Here is an example of what is generated from `Mono.Android.dll`: // Metadata.xml XPath interface reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']" [Register ("android/os/Parcelable", "", "Android.OS.IParcelableInvoker", ApiSince = 1)] public partial interface IParcelable : IJavaObject { + // Metadata.xml XPath field reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/field[@name='CONTENTS_FILE_DESCRIPTOR']" + [Register ("CONTENTS_FILE_DESCRIPTOR")] + public const int ContentsFileDescriptor = (int) 1; // Metadata.xml XPath method reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/method[@name='describeContents' and count(parameter)=0]" [Register ("describeContents", "()I", "GetDescribeContentsHandler:Android.OS.IParcelableInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] int DescribeContents (); // Metadata.xml XPath method reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/method[@name='writeToParcel' and count(parameter)=2 and parameter[1][@type='android.os.Parcel'] and parameter[2][@type='int']]" [Register ("writeToParcel", "(Landroid/os/Parcel;I)V", "GetWriteToParcel_Landroid_os_Parcel_IHandler:Android.OS.IParcelableInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] void WriteToParcel (Android.OS.Parcel dest, [global::Android.Runtime.GeneratedEnum] Android.OS.ParcelableWriteFlags flags); } There are several Android interfaces defined that only contain constants. Without Default-Interface-Methods, we have not been generating these interfaces because there was nothing we could generate for them. (These are referred to as `IsConstSugar`). Now we generate them, but they do not have `[Register]` on the interface and do not inherit from `IJavaObject` since they are not used for Java interop, e.g. [`android.icu.lang.UProperty.NameChoice][0]: [0]: https://developer.android.com/reference/android/icu/lang/UProperty.NameChoice // Metadata.xml XPath interface reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']" public partial interface IUPropertyNameChoice { // Metadata.xml XPath field reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']/field[@name='LONG']" [Register ("LONG")] public const int Long = (int) 1; // Metadata.xml XPath field reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']/field[@name='SHORT']" [Register ("SHORT")] public const int Short = (int) 0; } Here is the diff of this change run on `Mono.Android.dll`: https://gist.github.com/jpobst/7aa0bb1a01975a56038cb7ab12ecdb1c --- tools/generator/CodeGenerationOptions.cs | 1 + tools/generator/CodeGenerator.cs | 3 +- tools/generator/CodeGeneratorOptions.cs | 4 + .../CodeGenerator.cs | 129 +++++++++++------- .../InterfaceGen.cs | 10 ++ .../WriteConstSugarInterfaceFields.txt | 18 +++ .../JavaInterop1/WriteInterfaceFields.txt | 14 ++ .../WriteConstSugarInterfaceFields.txt | 18 +++ .../XamarinAndroid/WriteInterfaceFields.txt | 14 ++ .../Tests/Unit-Tests/CodeGeneratorTestBase.cs | 59 ++++++++ .../Tests/Unit-Tests/CodeGeneratorTests.cs | 60 ++------ .../DefaultInterfaceMethodsTests.cs | 74 ++++++++++ .../JavaInteropCodeGeneratorTests.cs | 11 -- .../Tests/Unit-Tests/SupportTypes.cs | 11 +- .../XamarinAndroidCodeGeneratorTests.cs | 11 -- tools/generator/Tests/generator-Tests.csproj | 16 ++- 16 files changed, 334 insertions(+), 119 deletions(-) create mode 100644 tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteConstSugarInterfaceFields.txt create mode 100644 tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterfaceFields.txt create mode 100644 tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteConstSugarInterfaceFields.txt create mode 100644 tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteInterfaceFields.txt create mode 100644 tools/generator/Tests/Unit-Tests/CodeGeneratorTestBase.cs create mode 100644 tools/generator/Tests/Unit-Tests/DefaultInterfaceMethodsTests.cs delete mode 100644 tools/generator/Tests/Unit-Tests/JavaInteropCodeGeneratorTests.cs delete mode 100644 tools/generator/Tests/Unit-Tests/XamarinAndroidCodeGeneratorTests.cs diff --git a/tools/generator/CodeGenerationOptions.cs b/tools/generator/CodeGenerationOptions.cs index 2dbc65786..0ced93b20 100644 --- a/tools/generator/CodeGenerationOptions.cs +++ b/tools/generator/CodeGenerationOptions.cs @@ -70,6 +70,7 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer) public bool UseShortFileNames { get; set; } public IList Gens {get;set;} public int ProductVersion { get; set; } + public bool SupportInterfaceConstants { get; set; } bool? buildingCoreAssembly; public bool BuildingCoreAssembly { diff --git a/tools/generator/CodeGenerator.cs b/tools/generator/CodeGenerator.cs index d5102ea59..739d68470 100644 --- a/tools/generator/CodeGenerator.cs +++ b/tools/generator/CodeGenerator.cs @@ -63,7 +63,8 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve UseGlobal = options.GlobalTypeNames, IgnoreNonPublicType = true, UseShortFileNames = options.UseShortFileNames, - ProductVersion = options.ProductVersion + ProductVersion = options.ProductVersion, + SupportInterfaceConstants = options.SupportInterfaceConstants, }; // Load reference libraries diff --git a/tools/generator/CodeGeneratorOptions.cs b/tools/generator/CodeGeneratorOptions.cs index 4e6721a7f..f9c1e5ff7 100644 --- a/tools/generator/CodeGeneratorOptions.cs +++ b/tools/generator/CodeGeneratorOptions.cs @@ -39,6 +39,7 @@ public CodeGeneratorOptions () public string MappingReportFile { get; set; } public bool OnlyRunApiXmlAdjuster { get; set; } public string ApiXmlAdjusterOutput { get; set; } + public bool SupportInterfaceConstants { get; set; } public static CodeGeneratorOptions Parse (string[] args) { @@ -85,6 +86,9 @@ public static CodeGeneratorOptions Parse (string[] args) { "sdk-platform|api-level=", "SDK Platform {VERSION}/API level.", v => opts.ApiLevel = v }, + { "lang-features=", + "For internal use. (Flags: interface-constants)", + v => opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true }, { "preserve-enums", "For internal use.", v => opts.PreserveEnums = v != null }, diff --git a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs index aa3eff9f7..7c6e4d834 100644 --- a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs +++ b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs @@ -441,61 +441,26 @@ internal virtual void WriteField (Field field, string indent, GenBase type) public void WriteInterface (InterfaceGen @interface, string indent, GenerationInfo gen_info) { opt.ContextTypes.Push (@interface); + // interfaces don't nest, so generate as siblings foreach (GenBase nest in @interface.NestedTypes) { WriteType (nest, indent, gen_info); writer.WriteLine (); } - var staticMethods = @interface.Methods.Where (m => m.IsStatic); - if (@interface.Fields.Any () || staticMethods.Any ()) { - string name = @interface.HasManagedName - ? @interface.Name.Substring (1) + "Consts" - : @interface.Name.Substring (1); - writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ()); - writer.WriteLine ("{0}public abstract class {1} : Java.Lang.Object {{", indent, name); - writer.WriteLine (); - writer.WriteLine ("{0}\tinternal {1} ()", indent, name); - writer.WriteLine ("{0}\t{{", indent); - writer.WriteLine ("{0}\t}}", indent); - - var seen = new HashSet (); - bool needsClassRef = WriteFields (@interface.Fields, indent + "\t", @interface, seen) || staticMethods.Any (); - foreach (var iface in @interface.GetAllImplementedInterfaces ().OfType ()) { - writer.WriteLine (); - writer.WriteLine ("{0}\t// The following are fields from: {1}", indent, iface.JavaName); - bool v = WriteFields (iface.Fields, indent + "\t", iface, seen); - needsClassRef = needsClassRef || v; - } - - foreach (var m in @interface.Methods.Where (m => m.IsStatic)) - WriteMethod (m, indent + "\t", @interface, true); - - if (needsClassRef) { - writer.WriteLine (); - WriteClassHandle (@interface, indent + "\t", name); - } + WriteInterfaceImplementedMembersAlternative (@interface, indent); - writer.WriteLine ("{0}}}", indent, @interface.Name); - writer.WriteLine (); + // If this interface is just fields and we can't generate any of them + // then we don't need to write the interface + if (@interface.IsConstSugar && @interface.GetGeneratableFields (opt).Count () == 0) + return; - if (!@interface.HasManagedName) { - writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ()); - writer.WriteLine ("{0}[global::System.Obsolete (\"Use the '{1}' type. This type will be removed in a future release.\")]", indent, name); - writer.WriteLine ("{0}public abstract class {1}Consts : {1} {{", indent, name); - writer.WriteLine (); - writer.WriteLine ("{0}\tprivate {1}Consts ()", indent, name); - writer.WriteLine ("{0}\t{{", indent); - writer.WriteLine ("{0}\t}}", indent); - writer.WriteLine ("{0}}}", indent); - writer.WriteLine (); - } - } + WriteInterfaceDeclaration (@interface, indent); + // If this interface is just constant fields we don't need to write all the invoker bits if (@interface.IsConstSugar) return; - WriteInterfaceDeclaration (@interface, indent); if (!@interface.AssemblyQualifiedName.Contains ('/')) WriteInterfaceExtensionsDeclaration (@interface, indent, null); WriteInterfaceInvoker (@interface, indent); @@ -547,11 +512,15 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent) if (@interface.IsDeprecated) writer.WriteLine ("{0}[ObsoleteAttribute (@\"{1}\")]", indent, @interface.DeprecatedComment); - writer.WriteLine ("{0}[Register (\"{1}\", \"\", \"{2}\"{3})]", indent, @interface.RawJniName, @interface.Namespace + "." + @interface.FullName.Substring (@interface.Namespace.Length + 1).Replace ('.', '/') + "Invoker", @interface.AdditionalAttributeString ()); + + if (!@interface.IsConstSugar) + writer.WriteLine ("{0}[Register (\"{1}\", \"\", \"{2}\"{3})]", indent, @interface.RawJniName, @interface.Namespace + "." + @interface.FullName.Substring (@interface.Namespace.Length + 1).Replace ('.', '/') + "Invoker", @interface.AdditionalAttributeString ()); + if (@interface.TypeParameters != null && @interface.TypeParameters.Any ()) writer.WriteLine ("{0}{1}", indent, @interface.TypeParameters.ToGeneratedAttributeString ()); - writer.WriteLine ("{0}{1} partial interface {2} : {3} {{", indent, @interface.Visibility, @interface.Name, - @interface.Interfaces.Count == 0 || sb.Length == 0 ? "IJavaObject" : sb.ToString ()); + writer.WriteLine ("{0}{1} partial interface {2}{3} {{", indent, @interface.Visibility, @interface.Name, + @interface.IsConstSugar ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : IJavaObject" : " : " + sb.ToString ()); + WriteInterfaceFields (@interface, indent + "\t"); writer.WriteLine (); WriteInterfaceProperties (@interface, indent + "\t"); WriteInterfaceMethods (@interface, indent + "\t"); @@ -709,6 +678,74 @@ public void WriteInterfaceExtensionsDeclaration (InterfaceGen @interface, string writer.WriteLine (); } + public void WriteInterfaceFields (InterfaceGen iface, string indent) + { + // Interface fields are only supported with DIM + if (!opt.SupportInterfaceConstants) + return; + + var seen = new HashSet (); + var fields = iface.GetGeneratableFields (opt).ToList (); + + WriteFields (fields, indent, iface, seen); + } + + public void WriteInterfaceImplementedMembersAlternative (InterfaceGen @interface, string indent) + { + // Historically .NET has not allowed interface implemented fields or constants, so we + // initially worked around that by moving them to an abstract class, generally + // IMyInterface -> MyInterfaceConsts + // This was later expanded to accomodate static interface methods, creating a more appropriately named class + // IMyInterface -> MyInterface + // In this case the XXXConsts class is [Obsolete]'d and simply inherits from the newer class + // in order to maintain backward compatibility. + var staticMethods = @interface.Methods.Where (m => m.IsStatic); + + if (@interface.Fields.Any () || staticMethods.Any ()) { + string name = @interface.HasManagedName + ? @interface.Name.Substring (1) + "Consts" + : @interface.Name.Substring (1); + writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ()); + writer.WriteLine ("{0}public abstract class {1} : Java.Lang.Object {{", indent, name); + writer.WriteLine (); + writer.WriteLine ("{0}\tinternal {1} ()", indent, name); + writer.WriteLine ("{0}\t{{", indent); + writer.WriteLine ("{0}\t}}", indent); + + var seen = new HashSet (); + bool needsClassRef = WriteFields (@interface.Fields, indent + "\t", @interface, seen) || staticMethods.Any (); + foreach (var iface in @interface.GetAllImplementedInterfaces ().OfType ()) { + writer.WriteLine (); + writer.WriteLine ("{0}\t// The following are fields from: {1}", indent, iface.JavaName); + bool v = WriteFields (iface.Fields, indent + "\t", iface, seen); + needsClassRef = needsClassRef || v; + } + + foreach (var m in @interface.Methods.Where (m => m.IsStatic)) + WriteMethod (m, indent + "\t", @interface, true); + + if (needsClassRef) { + writer.WriteLine (); + WriteClassHandle (@interface, indent + "\t", name); + } + + writer.WriteLine ("{0}}}", indent, @interface.Name); + writer.WriteLine (); + + if (!@interface.HasManagedName) { + writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ()); + writer.WriteLine ("{0}[global::System.Obsolete (\"Use the '{1}' type. This type will be removed in a future release.\")]", indent, name); + writer.WriteLine ("{0}public abstract class {1}Consts : {1} {{", indent, name); + writer.WriteLine (); + writer.WriteLine ("{0}\tprivate {1}Consts ()", indent, name); + writer.WriteLine ("{0}\t{{", indent); + writer.WriteLine ("{0}\t}}", indent); + writer.WriteLine ("{0}}}", indent); + writer.WriteLine (); + } + } + } + public void WriteInterfaceInvoker (InterfaceGen @interface, string indent) { writer.WriteLine ("{0}[global::Android.Runtime.Register (\"{1}\", DoNotGenerateAcw=true{2})]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ()); diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs index e7166e96e..3e6dd4f27 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -27,6 +28,15 @@ protected InterfaceGen (GenBaseSupport support) public bool HasManagedName => hasManagedName; + // These are fields that we currently support generating on the interface with DIM + public IEnumerable GetGeneratableFields (CodeGenerationOptions options) + { + if (!options.SupportInterfaceConstants) + return Enumerable.Empty (); + + return Fields.Where (f => !f.NeedsProperty && !(f.DeprecatedComment?.Contains ("constant will be removed") == true)); + } + public bool IsConstSugar { get { if (Methods.Count > 0 || Properties.Count > 0) diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteConstSugarInterfaceFields.txt b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteConstSugarInterfaceFields.txt new file mode 100644 index 000000000..fe8fb5d7e --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteConstSugarInterfaceFields.txt @@ -0,0 +1,18 @@ +// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']" +public partial interface IMyInterface { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']" + [Register ("MyConstantField")] + public const int MyConstantField = (int) 7; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantStringField']" + [Register ("MyConstantStringField")] + public const string MyConstantStringField = (string) "hello"; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyDeprecatedField']" + [Register ("MyDeprecatedField")] + [Obsolete ("")] + public const int MyDeprecatedField = (int) 7; + +} + diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterfaceFields.txt b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterfaceFields.txt new file mode 100644 index 000000000..085ad91e2 --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterfaceFields.txt @@ -0,0 +1,14 @@ +// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']" +[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")] +public partial interface IMyInterface : IJavaObject { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']" + [Register ("MyConstantField")] + public const int MyConstantField = (int) 7; + + // Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]" + [Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")] + void DoSomething (); + +} + diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteConstSugarInterfaceFields.txt b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteConstSugarInterfaceFields.txt new file mode 100644 index 000000000..fe8fb5d7e --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteConstSugarInterfaceFields.txt @@ -0,0 +1,18 @@ +// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']" +public partial interface IMyInterface { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']" + [Register ("MyConstantField")] + public const int MyConstantField = (int) 7; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantStringField']" + [Register ("MyConstantStringField")] + public const string MyConstantStringField = (string) "hello"; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyDeprecatedField']" + [Register ("MyDeprecatedField")] + [Obsolete ("")] + public const int MyDeprecatedField = (int) 7; + +} + diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteInterfaceFields.txt b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteInterfaceFields.txt new file mode 100644 index 000000000..085ad91e2 --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/XamarinAndroid/WriteInterfaceFields.txt @@ -0,0 +1,14 @@ +// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']" +[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")] +public partial interface IMyInterface : IJavaObject { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']" + [Register ("MyConstantField")] + public const int MyConstantField = (int) 7; + + // Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]" + [Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")] + void DoSomething (); + +} + diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorTestBase.cs b/tools/generator/Tests/Unit-Tests/CodeGeneratorTestBase.cs new file mode 100644 index 000000000..d525fb7d8 --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorTestBase.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using MonoDroid.Generation; +using NUnit.Framework; + +namespace generatortests.Unit_Tests +{ + abstract class CodeGeneratorTestBase + { + protected CodeGenerator generator; + protected StringBuilder builder; + protected StringWriter writer; + protected CodeGenerationOptions options; + + [SetUp] + public void SetUp () + { + builder = new StringBuilder (); + writer = new StringWriter (builder); + options = CreateOptions (); + + generator = options.CreateCodeGenerator (writer); + } + + [TearDown] + public void TearDown () + { + writer.Dispose (); + } + + protected virtual CodeGenerationOptions CreateOptions () + { + return new CodeGenerationOptions { + CodeGenerationTarget = Target, + }; + } + + protected abstract Xamarin.Android.Binder.CodeGenerationTarget Target { get; } + + // Get the test results from "Common" for tests with the same results regardless of Target + protected string GetExpected (string testName) + { + var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); + + return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", "Common", $"{testName}.txt")).NormalizeLineEndings (); + } + + // Get the test results from "JavaInterop1" or "XamarinAndroid" for tests with the different results per Target + protected string GetTargetedExpected (string testName) + { + var target = Target.ToString (); + var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); + + return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", target, $"{testName}.txt")).NormalizeLineEndings (); + } + } +} diff --git a/tools/generator/Tests/Unit-Tests/CodeGeneratorTests.cs b/tools/generator/Tests/Unit-Tests/CodeGeneratorTests.cs index 8b947cf88..c36f99923 100644 --- a/tools/generator/Tests/Unit-Tests/CodeGeneratorTests.cs +++ b/tools/generator/Tests/Unit-Tests/CodeGeneratorTests.cs @@ -1,40 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; using generatortests.Unit_Tests; using MonoDroid.Generation; using NUnit.Framework; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; +using Xamarin.Android.Binder; namespace generatortests { - abstract class CodeGeneratorTests + [TestFixture] + class JavaInteropCodeGeneratorTests : CodeGeneratorTests { - protected CodeGenerator generator; - protected StringBuilder builder; - protected StringWriter writer; - protected CodeGenerationOptions options; - - [SetUp] - public void SetUp () - { - builder = new StringBuilder (); - writer = new StringWriter (builder); - options = new CodeGenerationOptions { - CodeGenerationTarget = Target - }; - generator = options.CreateCodeGenerator (writer); - } - - [TearDown] - public void TearDown () - { - writer.Dispose (); - } + protected override CodeGenerationTarget Target => CodeGenerationTarget.JavaInterop1; + } - protected abstract Xamarin.Android.Binder.CodeGenerationTarget Target { get; } + [TestFixture] + class XamarinAndroidCodeGeneratorTests : CodeGeneratorTests + { + protected override CodeGenerationTarget Target => CodeGenerationTarget.XamarinAndroid; + } + abstract class CodeGeneratorTests : CodeGeneratorTestBase + { [Test] public void WriteCharSequenceEnumerator () { @@ -840,22 +827,5 @@ public void WritePropertyInvoker () Assert.AreEqual (GetExpected (nameof (WritePropertyInvoker)), writer.ToString ().NormalizeLineEndings ()); } - - // Get the test results from "Common" for tests with the same results regardless of Target - string GetExpected (string testName) - { - var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); - - return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", "Common", $"{testName}.txt")).NormalizeLineEndings (); - } - - // Get the test results from "JavaInterop1" or "XamarinAndroid" for tests with the different results per Target - string GetTargetedExpected (string testName) - { - var target = Target.ToString (); - var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); - - return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", target, $"{testName}.txt")).NormalizeLineEndings (); - } } } diff --git a/tools/generator/Tests/Unit-Tests/DefaultInterfaceMethodsTests.cs b/tools/generator/Tests/Unit-Tests/DefaultInterfaceMethodsTests.cs new file mode 100644 index 000000000..6fd050802 --- /dev/null +++ b/tools/generator/Tests/Unit-Tests/DefaultInterfaceMethodsTests.cs @@ -0,0 +1,74 @@ +using System; +using generatortests.Unit_Tests; +using MonoDroid.Generation; +using NUnit.Framework; + +namespace generatortests +{ + [TestFixture] + class JavaInteropDefaultInterfaceMethodsTests : DefaultInterfaceMethodsTests + { + protected override Xamarin.Android.Binder.CodeGenerationTarget Target => Xamarin.Android.Binder.CodeGenerationTarget.JavaInterop1; + } + + [TestFixture] + class XamarinAndroidDefaultInterfaceMethodsTests : DefaultInterfaceMethodsTests + { + protected override Xamarin.Android.Binder.CodeGenerationTarget Target => Xamarin.Android.Binder.CodeGenerationTarget.XamarinAndroid; + } + + abstract class DefaultInterfaceMethodsTests : CodeGeneratorTestBase + { + protected override CodeGenerationOptions CreateOptions () + { + var options = base.CreateOptions (); + + options.SupportInterfaceConstants = true; + + return options; + } + + [Test] + public void WriteInterfaceFields () + { + // This is an interface that has both fields and method declarations + var iface = SupportTypeBuilder.CreateEmptyInterface("java.code.IMyInterface"); + + iface.Fields.Add (new TestField ("int", "MyConstantField").SetConstant ().SetValue ("7")); + iface.Methods.Add (new TestMethod (iface, "DoSomething").SetAbstract ()); + + iface.Validate (options, new GenericParameterDefinitionList ()); + + options.ContextTypes.Push (iface); + generator.WriteInterfaceDeclaration (iface, string.Empty); + options.ContextTypes.Pop (); + + Assert.AreEqual (GetTargetedExpected (nameof (WriteInterfaceFields)), writer.ToString ().NormalizeLineEndings ()); + } + + [Test] + public void WriteConstSugarInterfaceFields () + { + // This is an interface that only has fields (IsConstSugar) + // We treat a little differenly because they don't need to interop with Java + var iface = SupportTypeBuilder.CreateEmptyInterface ("java.code.IMyInterface"); + + // These interface fields are supported and should be in the output + iface.Fields.Add (new TestField ("int", "MyConstantField").SetConstant ().SetValue ("7")); + iface.Fields.Add (new TestField ("java.lang.String", "MyConstantStringField").SetConstant ().SetValue ("\"hello\"")); + iface.Fields.Add (new TestField ("int", "MyDeprecatedField").SetConstant ().SetValue ("7").SetDeprecated ()); + + // These interface fields are not supported and should be ignored + iface.Fields.Add (new TestField ("int", "MyDeprecatedEnumField").SetConstant ().SetValue ("MyEnumValue").SetDeprecated ("This constant will be removed in the future version.")); + iface.Fields.Add (new TestField ("int", "MyStaticField").SetStatic ().SetValue ("7")); + + iface.Validate (options, new GenericParameterDefinitionList ()); + + options.ContextTypes.Push (iface); + generator.WriteInterfaceDeclaration (iface, string.Empty); + options.ContextTypes.Pop (); + + Assert.AreEqual (GetTargetedExpected (nameof (WriteConstSugarInterfaceFields)), writer.ToString ().NormalizeLineEndings ()); + } + } +} diff --git a/tools/generator/Tests/Unit-Tests/JavaInteropCodeGeneratorTests.cs b/tools/generator/Tests/Unit-Tests/JavaInteropCodeGeneratorTests.cs deleted file mode 100644 index f571508b5..000000000 --- a/tools/generator/Tests/Unit-Tests/JavaInteropCodeGeneratorTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NUnit.Framework; -using Xamarin.Android.Binder; - -namespace generatortests -{ - [TestFixture] - class JavaInteropCodeGeneratorTests : CodeGeneratorTests - { - protected override CodeGenerationTarget Target => CodeGenerationTarget.JavaInterop1; - } -} diff --git a/tools/generator/Tests/Unit-Tests/SupportTypes.cs b/tools/generator/Tests/Unit-Tests/SupportTypes.cs index 684a2cd45..16dd2198d 100644 --- a/tools/generator/Tests/Unit-Tests/SupportTypes.cs +++ b/tools/generator/Tests/Unit-Tests/SupportTypes.cs @@ -101,6 +101,12 @@ public TestField SetVisibility (string visibility) return this; } + public TestField SetValue (string value) + { + this.value = value; + return this; + } + public override bool IsDeprecated => isDeprecated; public override string DeprecatedComment => deprecatedComment; @@ -352,7 +358,7 @@ public static TestCtor CreateConstructor (GenBase parent, string methodName, Cod return ctor; } - public static TestInterface CreateEmptyInterface (string interfaceName, CodeGenerationOptions options) + public static TestInterface CreateEmptyInterface (string interfaceName) { var iface = new TestInterface (null, interfaceName); @@ -361,7 +367,7 @@ public static TestInterface CreateEmptyInterface (string interfaceName, CodeGene public static TestInterface CreateInterface (string interfaceName, CodeGenerationOptions options) { - var iface = CreateEmptyInterface (interfaceName, null); + var iface = CreateEmptyInterface (interfaceName); iface.Properties.Add (CreateProperty (iface, "Count", "int", options)); iface.Properties.Add (CreateProperty (iface, "Key", "java.lang.String", options)); @@ -401,7 +407,6 @@ public static Property CreateProperty (GenBase parent, string propertyName, stri Setter = CreateMethod (parent, $"set_{propertyName}", options, "void", isStatic, isAbstract, parameters: new Parameter ("value", propertyType, propertyType, false)) }; - return prop; } diff --git a/tools/generator/Tests/Unit-Tests/XamarinAndroidCodeGeneratorTests.cs b/tools/generator/Tests/Unit-Tests/XamarinAndroidCodeGeneratorTests.cs deleted file mode 100644 index 599822139..000000000 --- a/tools/generator/Tests/Unit-Tests/XamarinAndroidCodeGeneratorTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NUnit.Framework; -using Xamarin.Android.Binder; - -namespace generatortests -{ - [TestFixture] - class XamarinAndroidCodeGeneratorTests : CodeGeneratorTests - { - protected override CodeGenerationTarget Target => CodeGenerationTarget.XamarinAndroid; - } -} diff --git a/tools/generator/Tests/generator-Tests.csproj b/tools/generator/Tests/generator-Tests.csproj index f03653d95..d7650639e 100644 --- a/tools/generator/Tests/generator-Tests.csproj +++ b/tools/generator/Tests/generator-Tests.csproj @@ -69,13 +69,13 @@ + - + - @@ -208,6 +208,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -232,6 +235,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -310,6 +316,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -358,6 +367,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest