Skip to content

Commit

Permalink
[generator] Obsolete&SupportedOSPlatform attributes on enum members (#…
Browse files Browse the repository at this point in the history
…1066)

Fixes: #1037

Adds support for emitting `[Obsolete]`, `[ObsoletedOSPlatform]`, and
`[SupportedOSPlatform]` custom attributes on enum members.

~~ [SupportedOSPlatform] ~~

The data for `[SupportedOSPlatform]` comes from the values provided
in the enum-mapping file (b00e644), e.g.
[`xamarin-android/src/Mono.Android/map.csv`][0]:

	// src/Mono.Android/map.csv
	E,29,android/media/MediaRecorder$AudioEncoder.OPUS,7,Android.Media.AudioEncoder,Opus,keep,

The `29` is the API in which we added the enum, which becomes
`[SupportedOSPlatform("android-29.0")]`.

~~ [Obsolete] and [ObsoletedOSPlatform] ~~

The data for the "obsolete" attributes comes from the `deprecated`
and `deprecated-since` attributes on the original field in the
`api.xml`, if it can be found.  For example, given:

	<class name="AccessibilityServiceInfo" jni-signature="Landroid/accessibilityservice/AccessibilityServiceInfo;" …>
	    <field
	            deprecated="deprecated"
	            final="true"
	            name="CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY"
	            jni-signature="I"
	            static="true"
	            transient="false"
	            type="int"
	            type-generic-aware="int"
	            value="4"
	            visibility="public"
	            volatile="false"
	            deprecated-since="26"
	    />
	</class>

in combination with these `map.csv` (b00e644) entries:

	A,0,,0,Android.AccessibilityServices.AccessibilityServiceCapabilities,None,remove,
	E,18,android/accessibilityservice/AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY,4,Android.AccessibilityServices.AccessibilityServiceCapabilities,CanRequestEnhancedWebAccessibility,remove,

results in:

	/* partial */ enum AccessibilityServiceCapabilities {
	    // When using classic obsolete attributes:
	    [global::System.Obsolete("deprecated")]

	    // When using new obsolete attributes:
	    [global::System.Runtime.Versioning.ObsoletedOSPlatform("android31.0")]
	    CanRequestEnhancedWebAccessibility = 4,
	}

One wrinkle is we may have obsoleted the field because we want the
user to use the enum instead:

	<field
	    deprecated='This constant will be removed in the future version. Use Android.App.RecentTaskFlags enum directly instead of this field.'
	    name='RECENT_IGNORE_UNAVAILABLE'
	    …
	/>

We need to detect this message and not obsolete the enum in this case.

[0]: https://github.com/xamarin/xamarin-android/blob/17213ea184e23a9020451b265fec459558278489/src/Mono.Android/map.csv
  • Loading branch information
jpobst authored Dec 14, 2022
1 parent 15c8879 commit 5e6209e
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 21 deletions.
87 changes: 80 additions & 7 deletions tests/generator-Tests/Unit-Tests/EnumGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Java.Interop.Tools.Generator.Enumification;
using MonoDroid.Generation;
using NUnit.Framework;
using NUnit.Framework.Internal;
using Xamarin.Android.Binder;

namespace generatortests
{
[TestFixture]
class EnumGeneratorTests
class EnumGeneratorTests : CodeGeneratorTestBase
{
protected EnumGenerator generator;
protected StringBuilder builder;
protected StringWriter writer;
protected new EnumGenerator generator;

protected override CodeGenerationTarget Target => CodeGenerationTarget.XAJavaInterop1;

[SetUp]
public void SetUp ()
public new void SetUp ()
{
builder = new StringBuilder ();
writer = new StringWriter (builder);
Expand Down Expand Up @@ -60,7 +62,78 @@ public void WriteEnumWithGens ()
Assert.AreEqual (GetExpected (nameof (WriteEnumWithGens)), writer.ToString ().NormalizeLineEndings ());
}

protected string GetExpected (string testName)
[Test]
public void ObsoletedOSPlatformAttributeSupport ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='android.app' jni-name='android/app'>
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='ActivityManager' static='false' visibility='public' jni-signature='Landroid/app/ActivityManager;'>
<field deprecated='deprecated' final='true' name='RECENT_IGNORE_UNAVAILABLE' jni-signature='Ljava/lang/String;' static='true' transient='false' type='java.lang.String' type-generic-aware='java.lang.String' value='&quot;android.permission.BIND_CHOOSER_TARGET_SERVICE&quot;' visibility='public' volatile='false' deprecated-since='31' api-since='30' />
</class>
</package>
</api>";

options.UseObsoletedOSPlatformAttributes = true;

var enu = CreateEnum ();
var gens = ParseApiDefinition (xml);

generator.WriteEnumeration (options, enu, gens.ToArray ());

// Ensure [ObsoletedOSPlatform] and [SupportedOSPlatform] are written
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("[global::System.Runtime.Versioning.SupportedOSPlatformAttribute(\"android30.0\")][global::System.Runtime.Versioning.ObsoletedOSPlatform(\"android31.0\")]WithExcluded=1"), writer.ToString ());
}

[Test]
public void ObsoleteAttributeSupport ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='android.app' jni-name='android/app'>
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='ActivityManager' static='false' visibility='public' jni-signature='Landroid/app/ActivityManager;'>
<field deprecated='deprecated' final='true' name='RECENT_IGNORE_UNAVAILABLE' jni-signature='Ljava/lang/String;' static='true' transient='false' type='java.lang.String' type-generic-aware='java.lang.String' value='&quot;android.permission.BIND_CHOOSER_TARGET_SERVICE&quot;' visibility='public' volatile='false' deprecated-since='31' api-since='30' />
</class>
</package>
</api>";

var enu = CreateEnum ();
var gens = ParseApiDefinition (xml);

generator.WriteEnumeration (options, enu, gens.ToArray ());

// Ensure [Obsolete] is written
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("[global::System.Obsolete(@\"deprecated\")]WithExcluded=1"), writer.ToString ());
}

[Test]
public void ObsoleteFieldButNotEnumAttributeSupport ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='android.app' jni-name='android/app'>
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='ActivityManager' static='false' visibility='public' jni-signature='Landroid/app/ActivityManager;'>
<field deprecated='This constant will be removed in the future version. Use Android.App.RecentTaskFlags enum directly instead of this field.' final='true' name='RECENT_IGNORE_UNAVAILABLE' jni-signature='Ljava/lang/String;' static='true' transient='false' type='java.lang.String' type-generic-aware='java.lang.String' value='&quot;android.permission.BIND_CHOOSER_TARGET_SERVICE&quot;' visibility='public' volatile='false' deprecated-since='31' api-since='30' />
</class>
</package>
</api>";

var enu = CreateEnum ();
var gens = ParseApiDefinition (xml);

generator.WriteEnumeration (options, enu, gens.ToArray ());

// [Obsolete] should not be written because the value isn't deprecated, just the _field_ is deprecated because we want people to use the enum instead
Assert.False (writer.ToString ().NormalizeLineEndings ().Contains ("[global::System.Obsolete(@\"deprecated\")]WithExcluded=1"), writer.ToString ());
}

protected new string GetExpected (string testName)
{
var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location);

Expand All @@ -71,7 +144,7 @@ protected string GetExpected (string testName)
{
var enu = new EnumMappings.EnumDescription {
Members = new List<ConstantEntry> {
new ConstantEntry { EnumMember = "WithExcluded", Value = "1", JavaSignature = "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE" },
new ConstantEntry { EnumMember = "WithExcluded", Value = "1", JavaSignature = "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE", ApiLevel = 30 },
new ConstantEntry { EnumMember = "IgnoreUnavailable", Value = "2", JavaSignature = "android/app/ActivityManager.RECENT_WITH_EXCLUDED" }
},
BitField = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,36 +50,43 @@ EnumWriter CreateWriter (CodeGenerationOptions opt, KeyValuePair<string, EnumDes
Value = member.Value.Trim (),
};

// Try to find the original field in our model
var managedMember = FindManagedMember (enu.Value, member, gens);
var managedMemberName = managedMember != null ? $"{managedMember.Value.Cls.FullName}.{managedMember.Value.Field.Name}" : null;

if (opt.CodeGenerationTarget != CodeGenerationTarget.JavaInterop1)
m.Attributes.Add (new IntDefinitionAttr (managedMember, StripExtraInterfaceSpec (member.JavaSignature)));
m.Attributes.Add (new IntDefinitionAttr (managedMemberName, StripExtraInterfaceSpec (member.JavaSignature)));

SourceWriterExtensions.AddSupportedOSPlatform (m.Attributes, member.ApiLevel, opt);

// Some of our source fields may have been marked with:
// "This constant will be removed in the future version. Use XXX enum directly instead of this field."
// We don't want this message to propogate to the enum.
if (managedMember != null && managedMember.Value.Field?.DeprecatedComment?.Contains ("enum directly instead of this field") == false)
SourceWriterExtensions.AddObsolete (m.Attributes, managedMember.Value.Field.DeprecatedComment, opt, deprecatedSince: managedMember.Value.Field.DeprecatedSince);

enoom.Members.Add (m);
}

return enoom;
}

string FindManagedMember (EnumDescription desc, ConstantEntry member, IEnumerable<GenBase> gens)
WeakReference cache_found_class;

(GenBase Cls, Field Field)? FindManagedMember (EnumDescription desc, ConstantEntry constant, IEnumerable<GenBase> gens)
{
if (desc.FieldsRemoved)
return null;

var jniMember = member.JavaSignature;
var jniMember = constant.JavaSignature;

if (string.IsNullOrWhiteSpace (jniMember)) {
// enum values like "None" falls here.
return null;
}
return FindManagedMember (jniMember, gens);
}

WeakReference cache_found_class;
ParseJniMember (jniMember, out var package, out var type, out var member);

string FindManagedMember (string jniMember, IEnumerable<GenBase> gens)
{
string package, type, member;
ParseJniMember (jniMember, out package, out type, out member);
var fullJavaType = (string.IsNullOrEmpty (package) ? "" : package + ".") + type;

var cls = cache_found_class != null ? cache_found_class.Target as GenBase : null;
Expand All @@ -96,7 +103,7 @@ string FindManagedMember (string jniMember, IEnumerable<GenBase> gens)
// The field was not found e.g. removed by metadata fixup.
return null;
}
return cls.FullName + "." + fld.Name;
return (cls, fld);
}

internal void ParseJniMember (string jniMember, out string package, out string type, out string member)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,14 @@ public static void AddParameterListCallArgs (List<string> body, ParameterList pa
}

public static void AddSupportedOSPlatform (List<AttributeWriter> attributes, ApiVersionsSupport.IApiAvailability member, CodeGenerationOptions opt)
=> AddSupportedOSPlatform (attributes, member.ApiAvailableSince, opt);

public static void AddSupportedOSPlatform (List<AttributeWriter> attributes, int since, CodeGenerationOptions opt)
{
// There's no sense in writing say 'android15' because we do not support older APIs,
// so those APIs will be available in all of our versions.
if (member.ApiAvailableSince > 21 && opt.CodeGenerationTarget == Xamarin.Android.Binder.CodeGenerationTarget.XAJavaInterop1)
attributes.Add (new SupportedOSPlatformAttr (member.ApiAvailableSince));

if (since > 21 && opt.CodeGenerationTarget == Xamarin.Android.Binder.CodeGenerationTarget.XAJavaInterop1)
attributes.Add (new SupportedOSPlatformAttr (since));
}

public static void AddObsolete (List<AttributeWriter> attributes, string message, CodeGenerationOptions opt, bool forceDeprecate = false, bool isError = false, int? deprecatedSince = null)
Expand Down

0 comments on commit 5e6209e

Please sign in to comment.