Skip to content

Commit b0d170c

Browse files
authored
[generator] Only apply ConstSugar to Mono.Android.dll (#780)
Fixes: #752 Context: xamarin/GooglePlayServicesComponents#404 The Android API has an "interesting" pattern where they have an interface with some static constants, and then they inherit that interface in other types to inherit those constants. For example, [`android.provider.BaseColumns`][0]: // Java public interface BaseColumns { public static final String _COUNT = "_count"; public static final String _ID = "_id"; } This interface is inherited by ~115 other types, including other interfaces that only have static constant fields, such as [CalendarContract.CalendarSyncColumns][1]. This made for a very messy API when we bound it due the constants being copied into our alternative `IBaseColumnsConsts` and `ICalendarSyncColumnsConsts` classes. Therefore we decided to call this pattern "ConstSugar" and we had `generator` remove these interfaces, as we believed that they did not provide value to the managed API. However, this logic is applied to all binding libraries, and there can exist marker interfaces that meet the same criteria as ConstSugar, and they get silently removed from binding libraries. // Java public interface CanSerialize { public static final int VERSION = 1; } public interface CanJsonSerialize implements CanSerialize { } In this example we would not bind the `CanJsonSerialize` interface, because the presence of the `CanSerialize.VERSION` field led us to treat the `CanSerialize` interface as a ConstSugar type. Worse, an interface can go from not being considered as ConstSugar to triggering ConstSugar simply by *additions* to an API: if `CanSerialize` originally had no fields, both interfaces would be generated. Once `VERSION` is added, `CanJsonSerialize` is considered ConstSugar and will get removed. Since this logic is very specific to `Mono.Android`, we're going to use `CodeGenerationOptions.BuildingCoreAssembly` to determine if we're building `Mono.Android.dll`, and only run ConstSugar logic when true. This will prevent the logic from running on user binding projects and causing unexplained missing API for users. [0]: https://developer.android.com/reference/android/provider/BaseColumns [1]: https://developer.android.com/reference/android/provider/CalendarContract.CalendarSyncColumns?hl=en
1 parent a8f68e5 commit b0d170c

File tree

6 files changed

+35
-24
lines changed

6 files changed

+35
-24
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ protected override CodeGenerationOptions CreateOptions ()
4848
[Test]
4949
public void WriteConstSugarInterfaceFields ()
5050
{
51+
// Need a JLO class "FromXml" to trigger ConstSugar logic. (ie: this is "building" Mono.Android.dll)
52+
var klass = new TestClass ("java.lang.Object", "java.lang.Object") {
53+
FromXml = true,
54+
};
55+
56+
options.SymbolTable.AddType (klass);
57+
5158
// This is an interface that only has fields (IsConstSugar)
5259
// We treat a little differenly because they don't need to interop with Java
5360
var iface = SupportTypeBuilder.CreateEmptyInterface ("java.code.IMyInterface");

tools/generator/CodeGenerationOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer)
5959
public bool SupportNestedInterfaceTypes { get; set; }
6060
public bool SupportNullableReferenceTypes { get; set; }
6161
public bool UseShallowReferencedTypes { get; set; }
62+
public bool RemoveConstSugar => BuildingCoreAssembly;
6263

6364
bool? buildingCoreAssembly;
65+
// Basically this means "Are we building Mono.Android.dll?"
6466
public bool BuildingCoreAssembly {
6567
get {
6668
return buildingCoreAssembly ?? (buildingCoreAssembly = (SymbolTable.Lookup ("java.lang.Object") is ClassGen gen && gen.FromXml)).Value;

tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void WriteClass (ClassGen @class, string indent, GenerationInfo gen_info)
5555
foreach (ISymbol isym in @class.Interfaces) {
5656
GenericSymbol gs = isym as GenericSymbol;
5757
InterfaceGen gen = (gs == null ? isym : gs.Gen) as InterfaceGen;
58-
if (gen != null && (gen.IsConstSugar || gen.RawVisibility != "public"))
58+
if (gen != null && (gen.IsConstSugar (opt) || gen.RawVisibility != "public"))
5959
continue;
6060
if (sb.Length > 0)
6161
sb.Append (", ");
@@ -465,13 +465,13 @@ public void WriteInterface (InterfaceGen @interface, string indent, GenerationIn
465465

466466
// If this interface is just fields and we can't generate any of them
467467
// then we don't need to write the interface
468-
if (@interface.IsConstSugar && @interface.GetGeneratableFields (opt).Count () == 0)
468+
if (@interface.IsConstSugar (opt) && @interface.GetGeneratableFields (opt).Count () == 0)
469469
return;
470470

471471
WriteInterfaceDeclaration (@interface, indent, gen_info);
472472

473473
// If this interface is just constant fields we don't need to write all the invoker bits
474-
if (@interface.IsConstSugar)
474+
if (@interface.IsConstSugar (opt))
475475
return;
476476

477477
if (!@interface.AssemblyQualifiedName.Contains ('/'))
@@ -514,7 +514,7 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent, G
514514
StringBuilder sb = new StringBuilder ();
515515
foreach (ISymbol isym in @interface.Interfaces) {
516516
InterfaceGen igen = (isym is GenericSymbol ? (isym as GenericSymbol).Gen : isym) as InterfaceGen;
517-
if (igen.IsConstSugar || igen.RawVisibility != "public")
517+
if (igen.IsConstSugar (opt) || igen.RawVisibility != "public")
518518
continue;
519519
if (sb.Length > 0)
520520
sb.Append (", ");
@@ -526,7 +526,7 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent, G
526526
if (@interface.IsDeprecated)
527527
writer.WriteLine ("{0}[ObsoleteAttribute (@\"{1}\")]", indent, @interface.DeprecatedComment);
528528

529-
if (!@interface.IsConstSugar) {
529+
if (!@interface.IsConstSugar (opt)) {
530530
var signature = string.IsNullOrWhiteSpace (@interface.Namespace)
531531
? @interface.FullName.Replace ('.', '/')
532532
: @interface.Namespace + "." + @interface.FullName.Substring (@interface.Namespace.Length + 1).Replace ('.', '/');
@@ -537,7 +537,7 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent, G
537537
if (@interface.TypeParameters != null && @interface.TypeParameters.Any ())
538538
writer.WriteLine ("{0}{1}", indent, @interface.TypeParameters.ToGeneratedAttributeString ());
539539
writer.WriteLine ("{0}{1} partial interface {2}{3} {{", indent, @interface.Visibility, @interface.Name,
540-
@interface.IsConstSugar ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : " + GetAllInterfaceImplements () : " : " + sb.ToString ());
540+
@interface.IsConstSugar (opt) ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : " + GetAllInterfaceImplements () : " : " + sb.ToString ());
541541

542542
if (opt.SupportDefaultInterfaceMethods && (@interface.HasDefaultMethods || @interface.HasStaticMethods))
543543
WriteClassHandle (@interface, indent + "\t", @interface.Name);

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,22 +158,24 @@ public IEnumerable<Field> GetGeneratableFields (CodeGenerationOptions options)
158158

159159
public bool HasStaticMethods => GetAllMethods ().Any (m => m.IsStatic);
160160

161-
public bool IsConstSugar {
162-
get {
163-
if (Methods.Count > 0 || Properties.Count > 0)
164-
return false;
161+
public bool IsConstSugar (CodeGenerationOptions options)
162+
{
163+
if (!options.RemoveConstSugar)
164+
return false;
165165

166-
foreach (InterfaceGen impl in GetAllDerivedInterfaces ())
167-
if (!impl.IsConstSugar)
168-
return false;
166+
if (Methods.Count > 0 || Properties.Count > 0)
167+
return false;
169168

170-
// Need to keep Java.IO.ISerializable as a "marker interface"; want to
171-
// hide android.provider.ContactsContract.DataColumnsWithJoins
172-
if (Fields.Count == 0 && Interfaces.Count == 0)
169+
foreach (InterfaceGen impl in GetAllDerivedInterfaces ())
170+
if (!impl.IsConstSugar (options))
173171
return false;
174172

175-
return true;
176-
}
173+
// Need to keep Java.IO.ISerializable as a "marker interface"; want to
174+
// hide android.provider.ContactsContract.DataColumnsWithJoins
175+
if (Fields.Count == 0 && Interfaces.Count == 0)
176+
return false;
177+
178+
return true;
177179
}
178180

179181
// If there is a property it cannot generate valid implementor, so reject this at least so far.

tools/generator/SourceWriters/BoundClass.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ void AddCharSequenceEnumerator (ClassGen klass)
145145
void AddImplementedInterfaces (ClassGen klass)
146146
{
147147
foreach (var isym in klass.Interfaces) {
148-
if ((!(isym is GenericSymbol gs) ? isym : gs.Gen) is InterfaceGen gen && (gen.IsConstSugar || gen.RawVisibility != "public"))
148+
if ((!(isym is GenericSymbol gs) ? isym : gs.Gen) is InterfaceGen gen && (gen.IsConstSugar (opt) || gen.RawVisibility != "public"))
149149
continue;
150150

151151
Implements.Add (opt.GetOutputName (isym.FullName));

tools/generator/SourceWriters/BoundInterface.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public BoundInterface (InterfaceGen iface, CodeGenerationOptions opt, CodeGenera
2626
// If this interface is just fields and we can't generate any of them
2727
// then we don't need to write the interface. We still keep this type
2828
// because it may have nested types or need an InterfaceMemberAlternativeClass.
29-
if (iface.IsConstSugar && iface.GetGeneratableFields (opt).Count () == 0) {
29+
if (iface.IsConstSugar (opt) && iface.GetGeneratableFields (opt).Count () == 0) {
3030
dont_generate = true;
3131
return;
3232
}
@@ -43,7 +43,7 @@ public BoundInterface (InterfaceGen iface, CodeGenerationOptions opt, CodeGenera
4343
if (iface.IsDeprecated)
4444
Attributes.Add (new ObsoleteAttr (iface.DeprecatedComment) { WriteAttributeSuffix = true, WriteEmptyString = true });
4545

46-
if (!iface.IsConstSugar) {
46+
if (!iface.IsConstSugar (opt)) {
4747
var signature = string.IsNullOrWhiteSpace (iface.Namespace)
4848
? iface.FullName.Replace ('.', '/')
4949
: iface.Namespace + "." + iface.FullName.Substring (iface.Namespace.Length + 1).Replace ('.', '/');
@@ -63,7 +63,7 @@ public BoundInterface (InterfaceGen iface, CodeGenerationOptions opt, CodeGenera
6363
AddNestedTypes (iface, opt, context, genInfo);
6464

6565
// If this interface is just constant fields we don't need to add all the invoker bits
66-
if (iface.IsConstSugar)
66+
if (iface.IsConstSugar (opt))
6767
return;
6868

6969
if (!iface.AssemblyQualifiedName.Contains ('/')) {
@@ -137,13 +137,13 @@ void AddInheritedInterfaces (InterfaceGen iface, CodeGenerationOptions opt)
137137
foreach (var isym in iface.Interfaces) {
138138
var igen = (isym is GenericSymbol ? (isym as GenericSymbol).Gen : isym) as InterfaceGen;
139139

140-
if (igen.IsConstSugar || igen.RawVisibility != "public")
140+
if (igen.IsConstSugar (opt) || igen.RawVisibility != "public")
141141
continue;
142142

143143
Implements.Add (opt.GetOutputName (isym.FullName));
144144
}
145145

146-
if (Implements.Count == 0 && !iface.IsConstSugar)
146+
if (Implements.Count == 0 && !iface.IsConstSugar (opt))
147147
Implements.AddRange (new [] { "IJavaObject", "IJavaPeerable" });
148148
}
149149

0 commit comments

Comments
 (0)