Skip to content

Commit fadbb82

Browse files
authored
[generator] Add support for @explicitInterface metadata (#1006)
Fixes: #1005 Context: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods#reabstraction Sometimes Java interfaces will "re-declare" methods from implemented interfaces: // Java interface /* java.lang. */ AutoCloseable { void close(); } interface /* java.io. */ Closeable { void close(); } Historically, this has been bound in C# by "ignoring" that the method was re-declared: // C# partial interface /* Java.Lang. */ IAutoCloseable { void Close(); } partial interface /* Java.IO. */ ICloseable : IAutoCloseable { void Close(); } This would result in a [CS0108 warning][0]: warning CS0108: 'ICloseable.Close()' hides inherited member 'IAutoCloseable.Close()'. Use the new keyword if hiding was intended. *Note*: in .NET for Android, this *particular* scenario doesn't happen, as [`metadata` is used to remove `AutoCloseable`][1]. This scenario happens for other methods, e.g.: warning CS0108: 'IChannel.Close()' hides inherited member 'ICloseable.Close()'. Consequently, the .NET for Android build ignores CS0108 warnings. This in turn means that e.g. `IChannel.Close()` is implicitly declared as `new`. With the introduction of [Default Interface Members in C# 8][2], it is possible to ["re-abstract"][3] a method declared in an implemented interface. However, in order to re-abstract a member, the member must be explicitly qualified: partial interface ICloseable : IAutoCloseable { abstract void IAutoCloseable.Close (); } The declaring type isn't something we have enough information to figure out automatically. Add support for `explicitInterface` metadata which allows the declaring interface to be explicitly specified. <attr path="/api/package[@name='java.io']/interface[@name='Closeable']/method[@name='close']" name="explicitInterface">IAutoCloseable</attr> Note this is not really a concept that `generator` "understands" it will simply output whatever is specified in this attribute. Thus the value is the *C#* interface name, not the *Java* interface name. For properties, placing `explicitInterface` on either the `getFoo` or `setFoo` methods will work. The getter value takes precedence if both are specified. This is implemented for both `interface` and `class` members, should there be other use-cases that can benefit from it. [0]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs0108 [1]: https://github.com/xamarin/xamarin-android/blob/59352f833846c563768f18dd72133be101b0ddc6/src/Mono.Android/metadata#L1393 [2]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods [3]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods#reabstraction
1 parent 3e4a3c4 commit fadbb82

File tree

10 files changed

+164
-0
lines changed

10 files changed

+164
-0
lines changed

tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,156 @@ public void InheritedInterfaceAsClass ()
352352

353353
Assert.Pass ("WriteType did not NRE");
354354
}
355+
356+
[Test]
357+
public void ExplicitInterfaceMetadata_InterfaceMethod ()
358+
{
359+
var xml = @"<api>
360+
<package name='java.lang' jni-name='java/lang'>
361+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
362+
</package>
363+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
364+
<interface abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyInterface' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyInterface;'>
365+
<method abstract='true' deprecated='not deprecated' final='false' name='countAffectedRows' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IRowCounter'></method>
366+
</interface>
367+
</package>
368+
</api>";
369+
370+
var gens = ParseApiDefinition (xml);
371+
var iface = gens.Single (g => g.Name == "IMyInterface");
372+
373+
generator.Context.ContextTypes.Push (iface);
374+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
375+
generator.Context.ContextTypes.Pop ();
376+
377+
// Ensure explicit interface was written
378+
Assert.True (writer.ToString ().Contains ("int IRowCounter.CountAffectedRows ("), $"was: `{writer}`");
379+
}
380+
381+
[Test]
382+
public void ExplicitInterfaceMetadata_InterfaceProperty ()
383+
{
384+
var xml = @"<api>
385+
<package name='java.lang' jni-name='java/lang'>
386+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
387+
</package>
388+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
389+
<interface abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyInterface' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyInterface;'>
390+
<method abstract='true' deprecated='not deprecated' final='false' name='getAge' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IHasAge'></method>
391+
</interface>
392+
</package>
393+
</api>";
394+
395+
var gens = ParseApiDefinition (xml);
396+
var iface = gens.Single (g => g.Name == "IMyInterface");
397+
398+
generator.Context.ContextTypes.Push (iface);
399+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
400+
generator.Context.ContextTypes.Pop ();
401+
402+
// Ensure explicit interface was written
403+
Assert.True (writer.ToString ().Contains ("int IHasAge.Age {"), $"was: `{writer}`");
404+
}
405+
406+
[Test]
407+
public void ExplicitInterfaceMetadata_ClassMethod ()
408+
{
409+
var xml = @"<api>
410+
<package name='java.lang' jni-name='java/lang'>
411+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
412+
</package>
413+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
414+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
415+
<method abstract='false' deprecated='not deprecated' final='false' name='countAffectedRows' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IRowCounter'></method>
416+
</class>
417+
</package>
418+
</api>";
419+
420+
var gens = ParseApiDefinition (xml);
421+
var iface = gens.Single (g => g.Name == "MyClass");
422+
423+
generator.Context.ContextTypes.Push (iface);
424+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
425+
generator.Context.ContextTypes.Pop ();
426+
427+
// Ensure explicit interface was written
428+
Assert.True (writer.ToString ().Contains ("int IRowCounter.CountAffectedRows ("), $"was: `{writer}`");
429+
}
430+
431+
[Test]
432+
public void ExplicitInterfaceMetadata_ClassProperty ()
433+
{
434+
var xml = @"<api>
435+
<package name='java.lang' jni-name='java/lang'>
436+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
437+
</package>
438+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
439+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
440+
<method abstract='false' deprecated='not deprecated' final='false' name='getAge' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IHasAge'></method>
441+
</class>
442+
</package>
443+
</api>";
444+
445+
var gens = ParseApiDefinition (xml);
446+
var iface = gens.Single (g => g.Name == "MyClass");
447+
448+
generator.Context.ContextTypes.Push (iface);
449+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
450+
generator.Context.ContextTypes.Pop ();
451+
452+
// Ensure explicit interface was written
453+
Assert.True (writer.ToString ().Contains ("int IHasAge.Age {"), $"was: `{writer}`");
454+
}
455+
456+
[Test]
457+
public void ExplicitInterfaceMetadata_AbstractClassMethod ()
458+
{
459+
var xml = @"<api>
460+
<package name='java.lang' jni-name='java/lang'>
461+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
462+
</package>
463+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
464+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
465+
<method abstract='true' deprecated='not deprecated' final='false' name='countAffectedRows' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IRowCounter'></method>
466+
</class>
467+
</package>
468+
</api>";
469+
470+
var gens = ParseApiDefinition (xml);
471+
var iface = gens.Single (g => g.Name == "MyClass");
472+
473+
generator.Context.ContextTypes.Push (iface);
474+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
475+
generator.Context.ContextTypes.Pop ();
476+
477+
// Ensure explicit interface was written
478+
Assert.True (writer.ToString ().Contains ("abstract int IRowCounter.CountAffectedRows ("), $"was: `{writer}`");
479+
}
480+
481+
[Test]
482+
public void ExplicitInterfaceMetadata_AbstractClassProperty ()
483+
{
484+
var xml = @"<api>
485+
<package name='java.lang' jni-name='java/lang'>
486+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
487+
</package>
488+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
489+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
490+
<method abstract='true' deprecated='not deprecated' final='false' name='getAge' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' explicitInterface='IHasAge'></method>
491+
</class>
492+
</package>
493+
</api>";
494+
495+
var gens = ParseApiDefinition (xml);
496+
var iface = gens.Single (g => g.Name == "MyClass");
497+
498+
generator.Context.ContextTypes.Push (iface);
499+
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
500+
generator.Context.ContextTypes.Pop ();
501+
502+
// Ensure explicit interface was written
503+
Assert.True (writer.ToString ().Contains ("abstract int IHasAge.Age {"), $"was: `{writer}`");
504+
}
355505
}
356506

357507
[TestFixture]

tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ public static Method CreateMethod (GenBase declaringType, XElement elem, CodeGen
348348
ArgsType = elem.Attribute ("argsType")?.Value,
349349
CustomAttributes = elem.XGetAttribute ("customAttributes"),
350350
Deprecated = elem.Deprecated (),
351+
ExplicitInterface = elem.XGetAttribute ("explicitInterface"),
351352
EventName = elem.Attribute ("eventName")?.Value,
352353
GenerateAsyncWrapper = elem.Attribute ("generateAsyncWrapper") != null,
353354
GenerateDispatchingSetter = elem.Attribute ("generateDispatchingSetter") != null,

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public Method (GenBase declaringType) : base (declaringType)
3333
public bool ReturnNotNull { get; set; }
3434
public ReturnValue RetVal { get; set; }
3535
public int SourceApiLevel { get; set; }
36+
public string ExplicitInterface { get; set; }
3637

3738
// it used to be private though...
3839
internal string AdjustedName => IsReturnCharSequence ? Name + "Formatted" : Name;

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Property.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,7 @@ public void AutoDetectEnumifiedOverrideProperties (AncestorDescendantCache cache
5858
}
5959
}
6060
}
61+
62+
public string ExplicitInterface => Getter?.ExplicitInterface ?? Setter?.ExplicitInterface;
6163
}
6264
}

tools/generator/SourceWriters/BoundAbstractProperty.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class BoundAbstractProperty : PropertyWriter
1919
public BoundAbstractProperty (GenBase gen, Property property, CodeGenerationOptions opt)
2020
{
2121
Name = property.AdjustedName;
22+
ExplicitInterfaceImplementation = property.ExplicitInterface;
2223
PropertyType = new TypeReferenceWriter (opt.GetTypeReferenceName (property.Getter.RetVal));
2324

2425
SetVisibility (property.Getter.RetVal.IsGeneric ? "protected" : property.Getter.Visibility);

tools/generator/SourceWriters/BoundInterfaceMethodDeclaration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public BoundInterfaceMethodDeclaration (Method method, string adapter, CodeGener
2222
this.opt = opt;
2323

2424
Name = method.AdjustedName;
25+
ExplicitInterfaceImplementation = method.ExplicitInterface;
26+
2527
ReturnType = new TypeReferenceWriter (opt.GetTypeReferenceName (method.RetVal));
2628
IsDeclaration = true;
2729

tools/generator/SourceWriters/BoundInterfacePropertyDeclaration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class BoundInterfacePropertyDeclaration : PropertyWriter
1616
public BoundInterfacePropertyDeclaration (GenBase gen, Property property, string adapter, CodeGenerationOptions opt)
1717
{
1818
Name = property.AdjustedName;
19+
ExplicitInterfaceImplementation = property.ExplicitInterface;
1920

2021
PropertyType = new TypeReferenceWriter (opt.GetTypeReferenceName (property));
2122
IsAutoProperty = true;

tools/generator/SourceWriters/BoundMethod.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public BoundMethod (GenBase type, Method method, CodeGenerationOptions opt, bool
4848
if (is_explicit)
4949
ExplicitInterfaceImplementation = GetDeclaringTypeOfExplicitInterfaceMethod (method.OverriddenInterfaceMethod);
5050

51+
// Allow user to override our explicit interface logic
52+
if (method.ExplicitInterface.HasValue ())
53+
ExplicitInterfaceImplementation = method.ExplicitInterface;
54+
5155
if ((IsVirtual || !IsOverride) && type.RequiresNew (method.AdjustedName, method))
5256
IsShadow = true;
5357

tools/generator/SourceWriters/BoundMethodAbstractDeclaration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public BoundMethodAbstractDeclaration (GenBase gen, Method method, CodeGeneratio
3636
}
3737

3838
Name = method.AdjustedName;
39+
ExplicitInterfaceImplementation = method.ExplicitInterface;
3940

4041
IsAbstract = true;
4142
IsShadow = impl.RequiresNew (method.Name, method);

tools/generator/SourceWriters/BoundProperty.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class BoundProperty : PropertyWriter
1919
public BoundProperty (GenBase gen, Property property, CodeGenerationOptions opt, bool withCallbacks = true, bool forceOverride = false)
2020
{
2121
Name = property.AdjustedName;
22+
ExplicitInterfaceImplementation = property.ExplicitInterface;
2223
PropertyType = new TypeReferenceWriter (opt.GetTypeReferenceName (property.Getter.RetVal));
2324

2425
SetVisibility (gen is InterfaceGen ? string.Empty : property.Getter.IsAbstract && property.Getter.RetVal.IsGeneric ? "protected" : (property.Setter ?? property.Getter).Visibility);

0 commit comments

Comments
 (0)