diff --git a/Xamarin.Android.slnx b/Xamarin.Android.slnx index 2c15cd1d963..9ec9f9fcdb6 100644 --- a/Xamarin.Android.slnx +++ b/Xamarin.Android.slnx @@ -62,6 +62,7 @@ + diff --git a/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs index ea69f86dfeb..c71f5dd524e 100644 --- a/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs +++ b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs @@ -2,11 +2,12 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; namespace Microsoft.Android.Build.Tasks { /// - /// Allocation-free helpers for rendering bytes as hexadecimal. + /// Allocation-free helpers for rendering values as hexadecimal. /// /// /// This file is also linked into Microsoft.Android.Sdk.TrimmableTypeMap, which @@ -65,6 +66,21 @@ public static void WriteHex (TextWriter writer, byte value, bool upperCase = tru writer.Write (GetHexValue (value & 0x0f, upperCase)); } + /// + /// Append to as exactly four + /// hexadecimal digits, without allocating. + /// + public static void WriteHex (StringBuilder builder, ushort value, bool upperCase = true) + { + if (builder == null) + throw new ArgumentNullException (nameof (builder)); + + builder.Append (GetHexValue (value >> 12, upperCase)); + builder.Append (GetHexValue ((value >> 8) & 0x0f, upperCase)); + builder.Append (GetHexValue ((value >> 4) & 0x0f, upperCase)); + builder.Append (GetHexValue (value & 0x0f, upperCase)); + } + /// /// Convert to a hexadecimal string, without allocating /// intermediate strings. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 29e8d56742a..f65be27397d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -65,6 +65,7 @@ public void Generate (JavaPeerInfo type, TextWriter writer, string? applicationJ { writer.NewLine = "\n"; WritePackageDeclaration (type, writer); + WriteAnnotations (type.Annotations, writer); WriteClassDeclaration (type, writer, applicationJavaClass); WriteStaticInitializer (type, writer); WriteConstructors (type, writer); @@ -176,6 +177,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams); string args = FormatArgumentList (ctorParams); + WriteAnnotations (ctor.Annotations, writer); writer.Write ($$""" public {{simpleClassName}} ({{parameters}}) { @@ -212,6 +214,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) static void WriteFields (JavaPeerInfo type, TextWriter writer) { foreach (var field in type.JavaFields) { + WriteAnnotations (field.Annotations, writer); writer.Write ('\t'); writer.Write (field.Visibility); writer.Write (' '); @@ -257,8 +260,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) } if (method.Connector != null && !method.IsExport) { + writer.WriteLine (); + WriteAnnotations (method.Annotations, writer); writer.Write ($$""" - @Override public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { @@ -270,8 +274,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) } else { string access = method.IsExport && method.JavaAccess != null ? method.JavaAccess : "public"; string staticKeyword = method.IsStatic ? "static " : ""; + writer.WriteLine (); + WriteAnnotations (method.Annotations, writer); writer.Write ($$""" - {{access}} {{staticKeyword}}{{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { {{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); @@ -283,6 +288,29 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) } } + static void WriteAnnotations (IReadOnlyList annotations, TextWriter writer) + { + foreach (var annotation in annotations) { + writer.Write ('@'); + writer.Write (annotation.Name); + if (annotation.Properties.Count > 0) { + writer.Write (" ("); + bool first = true; + foreach (var property in annotation.Properties) { + if (!first) { + writer.Write (", "); + } + writer.Write (property.Key); + writer.Write (" = "); + writer.Write (property.Value); + first = false; + } + writer.Write (')'); + } + writer.WriteLine (); + } + } + static void WriteGCUserPeerMethods (TextWriter writer) { writer.Write (""" diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index f16e61eb2cc..7628ad984ac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -678,6 +678,7 @@ sealed record ExportInfo { public IReadOnlyList? ThrownNames { get; init; } public string? SuperArgumentsString { get; init; } + public bool IsField { get; init; } public IReadOnlyList ParameterKinds { get; init; } = []; public ExportParameterKindInfo ReturnKind { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs new file mode 100644 index 00000000000..8ce19000586 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection.Metadata; +using System.Text; +using Microsoft.Android.Build.Tasks; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +sealed class JavaAnnotationParser +{ + sealed record AnnotationTypeInfo (string JavaName, IReadOnlyDictionary PropertyNames); + + static readonly IReadOnlyList noAnnotations = []; + + readonly IReadOnlyDictionary assemblies; + readonly Func resolveTypeName; + readonly Dictionary<(AssemblyIndex Index, EntityHandle Type), AnnotationTypeInfo?> annotationTypes = new (); + + public JavaAnnotationParser (IReadOnlyDictionary assemblies, Func resolveTypeName) + { + this.assemblies = assemblies; + this.resolveTypeName = resolveTypeName; + } + + public IReadOnlyList Parse (CustomAttributeHandleCollection attributes, AssemblyIndex index) + { + List? annotations = null; + foreach (var attributeHandle in attributes) { + var attribute = index.Reader.GetCustomAttribute (attributeHandle); + var annotationType = GetAnnotationType (attribute, index); + if (annotationType is null) { + continue; + } + + annotations ??= []; + annotations.Add (new JavaAnnotationInfo { + Name = annotationType.JavaName, + Properties = GetProperties (attribute, index, annotationType), + }); + } + return annotations ?? noAnnotations; + } + + static string? GetJavaName (TypeDefinition attributeType, AssemblyIndex index) + { + foreach (var markerHandle in attributeType.GetCustomAttributes ()) { + var marker = index.Reader.GetCustomAttribute (markerHandle); + if (!AssemblyIndex.IsCustomAttributeMatch (marker, index.Reader, "Android.Runtime", "AnnotationAttribute")) { + continue; + } + + var value = index.DecodeAttribute (marker); + return value.FixedArguments.Length > 0 ? value.FixedArguments [0].Value as string : null; + } + return null; + } + + IReadOnlyList> GetProperties ( + CustomAttribute attribute, + AssemblyIndex index, + AnnotationTypeInfo annotationType) + { + var properties = new List> (); + foreach (var property in index.DecodeAttribute (attribute).NamedArguments) { + if (property.Kind != CustomAttributeNamedArgumentKind.Property || property.Name is null) { + continue; + } + var propertyName = annotationType.PropertyNames.TryGetValue (property.Name, out var javaName) + ? javaName + : property.Name; + properties.Add (new KeyValuePair ( + propertyName, + ManagedValueToJavaSource (property.Type, property.Value) + )); + } + return properties; + } + + static IReadOnlyDictionary GetJavaPropertyNames (TypeDefinition attributeType, AssemblyIndex index) + { + var names = new Dictionary (StringComparer.Ordinal); + foreach (var propertyHandle in attributeType.GetProperties ()) { + var property = index.Reader.GetPropertyDefinition (propertyHandle); + var managedName = index.Reader.GetString (property.Name); + foreach (var attributeHandle in property.GetCustomAttributes ()) { + var attribute = index.Reader.GetCustomAttribute (attributeHandle); + if (!AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Android.Runtime", "RegisterAttribute")) { + continue; + } + var value = index.DecodeAttribute (attribute); + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string javaName) { + names [managedName] = javaName; + } + break; + } + } + return names; + } + + AnnotationTypeInfo? GetAnnotationType (CustomAttribute attribute, AssemblyIndex index) + { + EntityHandle typeHandle = default; + if (attribute.Constructor.Kind == HandleKind.MethodDefinition) { + typeHandle = index.Reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType (); + } else if (attribute.Constructor.Kind == HandleKind.MemberReference) { + typeHandle = index.Reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor).Parent; + } + + var key = (index, typeHandle); + if (typeHandle.IsNil || annotationTypes.TryGetValue (key, out var cached) && cached is null) { + return null; + } + if (cached is not null) { + return cached; + } + + TypeDefinition attributeType; + AssemblyIndex attributeIndex; + if (typeHandle.Kind == HandleKind.TypeDefinition) { + attributeType = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)typeHandle); + attributeIndex = index; + } else if (typeHandle.Kind == HandleKind.TypeReference) { + var typeReference = MetadataTypeNameResolver.GetTypeRefFromReference ( + index.Reader, + (TypeReferenceHandle)typeHandle, + index.AssemblyName, + rawTypeKind: 0 + ); + if (!assemblies.TryGetValue (typeReference.AssemblyName, out attributeIndex) || + !attributeIndex.TypesByFullName.TryGetValue (typeReference.ManagedTypeName, out var resolvedHandle)) { + annotationTypes [key] = null; + return null; + } + attributeType = attributeIndex.Reader.GetTypeDefinition (resolvedHandle); + } else { + annotationTypes [key] = null; + return null; + } + + var javaName = GetJavaName (attributeType, attributeIndex); + var result = javaName.IsNullOrEmpty () + ? null + : new AnnotationTypeInfo (javaName, GetJavaPropertyNames (attributeType, attributeIndex)); + annotationTypes [key] = result; + return result; + } + + string ManagedValueToJavaSource (string managedType, object? value) + { + if (value is null) { + return "null"; + } + if (managedType == "String" || managedType == "System.String") { + return ToJavaStringLiteral (value.ToString () ?? ""); + } + if (managedType == "System.Type" && value is string typeName) { + var javaName = resolveTypeName (typeName); + if (javaName is not null) { + return JniSignatureHelper.JniNameToJavaName (javaName) + ".class"; + } + throw new InvalidOperationException ($"Java annotation type value '{typeName}' does not resolve to a Java peer."); + } + if (value is bool boolean) { + return boolean ? "true" : "false"; + } + if (value is IFormattable formattable) { + return formattable.ToString (null, CultureInfo.InvariantCulture) ?? ""; + } + return value.ToString () ?? ""; + } + + static string ToJavaStringLiteral (string value) + { + var builder = new StringBuilder (value.Length + 2); + builder.Append ('"'); + foreach (char c in value) { + switch (c) { + case '"': + builder.Append ("\\\""); + break; + case '\\': + builder.Append ("\\\\"); + break; + case '\b': + builder.Append ("\\b"); + break; + case '\t': + builder.Append ("\\t"); + break; + case '\n': + builder.Append ("\\n"); + break; + case '\f': + builder.Append ("\\f"); + break; + case '\r': + builder.Append ("\\r"); + break; + default: + if (char.IsControl (c)) { + builder.Append ("\\u"); + HexUtilities.WriteHex (builder, c, upperCase: false); + } else { + builder.Append (c); + } + break; + } + } + builder.Append ('"'); + return builder.ToString (); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 5f591978de3..1dce5a4e48c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -63,6 +63,12 @@ public sealed record JavaPeerInfo /// public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); + /// + /// Java annotations forwarded from managed custom attributes decorated with + /// Android.Runtime.AnnotationAttribute. + /// + public IReadOnlyList Annotations { get; init; } = []; + public bool IsInterface { get; init; } public bool IsAbstract { get; init; } @@ -296,6 +302,20 @@ public sealed record MarshalMethodInfo /// new virtual while reusing the same JNI name and signature. /// public bool CallManagedMethodDirectly { get; init; } + + /// + /// Java annotations forwarded from the managed method or constructor. + /// + public IReadOnlyList Annotations { get; init; } = []; +} + +/// +/// Describes a Java annotation forwarded from a managed custom attribute. +/// +public sealed record JavaAnnotationInfo +{ + public required string Name { get; init; } + public IReadOnlyList> Properties { get; init; } = []; } /// @@ -344,6 +364,11 @@ public sealed record JavaConstructorInfo /// True when this Java constructor has a matching public managed constructor on the target type. /// public bool HasMatchingManagedCtor { get; init; } + + /// + /// Java annotations forwarded from the managed constructor. + /// + public IReadOnlyList Annotations { get; init; } = []; } /// @@ -376,6 +401,11 @@ public sealed record JavaFieldInfo /// Whether the field is static. /// public bool IsStatic { get; init; } + + /// + /// Java annotations forwarded from the managed field initializer method. + /// + public IReadOnlyList Annotations { get; init; } = []; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index a9b28686132..0376a54c98b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -33,6 +33,7 @@ enum HashedPackageNamingPolicy { readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; readonly bool errorOnCustomJavaObject; + readonly JavaAnnotationParser annotationParser; public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = true) { @@ -40,6 +41,7 @@ public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLog this.logger = logger; this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet (StringComparer.OrdinalIgnoreCase); this.errorOnCustomJavaObject = errorOnCustomJavaObject; + annotationParser = new JavaAnnotationParser (assemblyCache, ResolveTypeOfArgumentToJniName); } /// @@ -385,6 +387,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A IsFrameworkAssembly = frameworkAssemblyNames.Contains (index.AssemblyName), BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, + Annotations = annotationParser.Parse (typeDef.GetCustomAttributes (), index), IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, @@ -818,7 +821,7 @@ void CollectBasePropertyOverrides (TypeDefinition typeDef, AssemblyIndex index, continue; } - var baseRegistration = FindBaseRegisteredProperty (typeDef, index, getterName, getterDef); + var baseRegistration = FindBaseRegisteredProperty (typeDef, index, getterName, getterDef, index); if (baseRegistration is not null) { methods.Add (baseRegistration); alreadyRegistered.Add (sigKey); @@ -961,6 +964,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, ManagedMethodName = ".ctor", NativeCallbackName = "n_ctor", IsConstructor = true, + Annotations = annotationParser.Parse (baseCtor.Method.GetCustomAttributes (), baseCtor.Index), }); alreadyRegisteredSignatures.Add (signature); } @@ -1394,6 +1398,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con DeclaringTypeName = result.Value.DeclaringType.ManagedTypeName, DeclaringAssemblyName = result.Value.DeclaringType.AssemblyName, DeclaringType = result.Value.DeclaringType, + Annotations = annotationParser.Parse (derivedMethod.GetCustomAttributes (), index), }; } @@ -1402,7 +1407,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con /// matches the given getter name and has a compatible signature. /// MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, - string getterName, MethodDefinition derivedGetter, TypeRefData? currentTypeRef = null) + string getterName, MethodDefinition derivedGetter, AssemblyIndex derivedIndex, TypeRefData? currentTypeRef = null) { if (!TryResolveBaseType (typeDef, index, currentTypeRef, out var baseTypeDef, out _, out var baseIndex, out _, out _, out var baseTypeRef)) { return null; @@ -1446,13 +1451,14 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con DeclaringTypeName = baseTypeRef.ManagedTypeName, DeclaringAssemblyName = baseTypeRef.AssemblyName, DeclaringType = baseTypeRef, + Annotations = annotationParser.Parse (derivedGetter.GetCustomAttributes (), derivedIndex), }; } } // Keep walking the full base hierarchy so property overrides can inherit // [Register] metadata declared above an intermediate MCW base type. - return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter, baseTypeRef); + return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter, derivedIndex, baseTypeRef); } /// @@ -1554,6 +1560,7 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, CallManagedMethodDirectly = callManagedMethodDirectly, + Annotations = exportInfo?.IsField == true ? [] : annotationParser.Parse (methodDef.GetCustomAttributes (), index), }); } @@ -1878,7 +1885,7 @@ string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnly return ( new RegisterInfo { JniName = managedName, Signature = jniSig, Connector = "__export__", DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = null, SuperArgumentsString = null } + new ExportInfo { ThrownNames = null, SuperArgumentsString = null, IsField = true } ); } @@ -2481,6 +2488,7 @@ List BuildJavaConstructors (List marshal SuperArgumentsString = mm.SuperArgumentsString, HasMatchingManagedCtor = managedParams != null, ManagedParameterTypes = managedParams ?? [], + Annotations = mm.Annotations, }); ctorIndex++; } @@ -2592,6 +2600,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List (() => HexUtilities.WriteHex (writer: null, value: 0x00)); } + [Test] + public void WriteHex_StringBuilder_UInt16 () + { + var builder = new StringBuilder ("_"); + HexUtilities.WriteHex (builder, 0xabcd); + HexUtilities.WriteHex (builder, 0x012f, upperCase: false); + Assert.AreEqual ("_ABCD012f", builder.ToString ()); + } + + [Test] + public void WriteHex_StringBuilder_NullThrows () + { + Assert.Throws (() => HexUtilities.WriteHex (builder: null, value: 0x0000)); + } + [TestCase (0)] [TestCase (1)] public void WriteHex_Span_TooShortThrows (int length) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d824733c2f2..9f4c2460155 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -22,15 +22,27 @@ private protected static string TestFixtureAssemblyPath { } } + static string TestAttributeFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestAttributeFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestAttributeFixtures.dll not found at {fixtureAssembly}. Ensure the TestAttributeFixtures project builds."); + return fixtureAssembly; + } + } + static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var scanner = new JavaPeerScanner (); - var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); - var mdReader = peReader.GetMetadataReader (); - var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - var assemblies = new [] { (assemblyName, peReader) }; + using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath)); + var assemblies = new [] { + GetAssemblyInput (peReader), + GetAssemblyInput (attributePeReader), + }; var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); - peReader.Dispose (); return (peers, manifestInfo); }); @@ -40,12 +52,20 @@ private protected static List ScanFixtures (string packageNamingPo { using var scanner = new JavaPeerScanner (packageNamingPolicy); using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); - var mdReader = peReader.GetMetadataReader (); - var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - var assemblies = new [] { (assemblyName, peReader) }; + using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath)); + var assemblies = new [] { + GetAssemblyInput (peReader), + GetAssemblyInput (attributePeReader), + }; return scanner.Scan (assemblies); } + static (string Name, PEReader Reader) GetAssemblyInput (PEReader peReader) + { + var reader = peReader.GetMetadataReader (); + return (reader.GetString (reader.GetAssemblyDefinition ().Name), peReader); + } + private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo; private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 055b3c399e7..1e2351626f1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using Xunit; @@ -347,6 +348,53 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } + [Fact] + public void Generate_MarshalMethod_ForwardsJavaAnnotation () + { + var peer = FindFixtureByManagedName ("MyApp.MyWebViewHandler"); + var java = GenerateToString (peer); + + AssertContainsLine ("@android.webkit.JavascriptInterface\n\t@Override\n\tpublic void postMessage (java.lang.String p0)\n", java); + } + + [Fact] + public void Generate_ForwardsAnnotationsOnJcwMembers () + { + var typeJava = GenerateFixture ("my/app/MyHelper"); + AssertContainsLine ("@com.example.Custom (text = \"say \\\"hi\\\"\\\\path\\n\", Enabled = true, Number = 1.5)\npublic class MyHelper\n", typeJava); + + var constructorJava = GenerateFixture ("my/app/CustomView"); + AssertContainsLine ("@com.example.Custom\n\tpublic CustomView ()\n", constructorJava); + + var fieldJava = GenerateFixture ("my/app/ExportFieldExample"); + AssertContainsLine ("@com.example.Custom\n\tpublic java.lang.String VALUE = GetValue ();\n", fieldJava); + Assert.Equal (1, fieldJava.Split ("@com.example.Custom").Length - 1); + } + + [Fact] + public void Generate_AnnotationValues_UseInvariantCulture () + { + var originalCulture = CultureInfo.CurrentCulture; + try { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo ("fr-FR"); + var peer = FindFixtureByManagedName ("MyApp.MyHelper", "Crc64"); + var java = GenerateToString (peer); + + AssertContainsLine ("Number = 1.5", java); + } finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void Generate_PropertyOverride_ForwardsGetterAnnotation () + { + var peer = FindFixtureByManagedName ("MyApp.AnnotatedPropertyDerived"); + var java = GenerateToString (peer); + + AssertContainsLine ("@com.example.Custom\n\t@Override\n\tpublic int getValue ()\n", java); + } + [Fact] public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 212fa1be735..f42e38eb363 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -13,6 +13,7 @@ + @@ -23,6 +24,9 @@ + + false + false @@ -33,6 +37,7 @@ <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> + <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs new file mode 100644 index 00000000000..ff3a6d6ddcd --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs @@ -0,0 +1,65 @@ +using System; + +namespace Java.Interop +{ + public interface IJniNameProviderAttribute + { + string Name { get; } + } +} + +namespace Android.Runtime +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class AnnotationAttribute : Attribute + { + public string JavaName { get; } + + public AnnotationAttribute (string javaName) => JavaName = javaName; + } + + [AttributeUsage ( + AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | + AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, + AllowMultiple = false)] + public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public string? Signature { get; set; } + public string? Connector { get; set; } + public bool DoNotGenerateAcw { get; set; } + public int ApiSince { get; set; } + + public RegisterAttribute (string name) => Name = name; + + public RegisterAttribute (string name, string signature, string connector) + { + Name = name; + Signature = signature; + Connector = connector; + } + } +} + +namespace Android.Webkit +{ + [Android.Runtime.Annotation ("android.webkit.JavascriptInterface")] + [AttributeUsage (AttributeTargets.Method)] + public sealed class JavascriptInterfaceAttribute : Attribute + { + } +} + +namespace MyApp +{ + [Android.Runtime.Annotation ("com.example.Custom")] + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)] + public sealed class JavaAnnotationAttribute : Attribute + { + [Android.Runtime.Register ("text")] + public string? Text { get; set; } + + public bool Enabled { get; set; } + public double Number { get; set; } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj new file mode 100644 index 00000000000..b089adb3218 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj @@ -0,0 +1,10 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 050741c3e20..5f8a694b111 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -1,37 +1,7 @@ using System; -namespace Java.Interop -{ - public interface IJniNameProviderAttribute - { - string Name { get; } - } -} - namespace Android.Runtime { - [AttributeUsage ( - AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | - AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, - AllowMultiple = false)] - public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute - { - public string Name { get; } - public string? Signature { get; set; } - public string? Connector { get; set; } - public bool DoNotGenerateAcw { get; set; } - public int ApiSince { get; set; } - - public RegisterAttribute (string name) => Name = name; - - public RegisterAttribute (string name, string signature, string connector) - { - Name = name; - Signature = signature; - Connector = connector; - } - } - public enum JniHandleOwnership { DoNotTransfer = 0, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj index f7f4c72139b..c5f61f8238c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -10,4 +10,8 @@ true + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index ceedbc96fe6..c95ea7d5587 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -4,6 +4,7 @@ using Android.App; using Android.Content; using Android.Runtime; +using Android.Webkit; namespace Java.Lang { @@ -275,12 +276,41 @@ public MainActivity () { } } [Register ("my/app/MyHelper")] + [JavaAnnotation (Text = "say \"hi\"\\path\n", Enabled = true, Number = 1.5)] public class MyHelper : Java.Lang.Object { [Register ("doSomething", "()V", "GetDoSomethingHandler")] public virtual void DoSomething () { } } + [Register ("my/app/WebViewHandlerBase")] + public abstract class WebViewHandlerBase : Java.Lang.Object + { + [Register ("postMessage", "(Ljava/lang/String;)V", "GetPostMessage_Ljava_lang_String_Handler")] + public abstract void PostMessage (string? message); + } + + public class MyWebViewHandler : WebViewHandlerBase + { + [JavascriptInterface] + public override void PostMessage (string? message) { } + } + + [Register ("my/app/AnnotatedPropertyBase", DoNotGenerateAcw = true)] + public class AnnotatedPropertyBase : Java.Lang.Object + { + [Register ("getValue", "()I", "GetGetValueHandler")] + public virtual int Value => 0; + } + + public class AnnotatedPropertyDerived : AnnotatedPropertyBase + { + public override int Value { + [JavaAnnotation] + get => 1; + } + } + // Fixture for the trimmable typemap's [JniAddNativeMethodRegistrationAttribute] detection. // The trimmable typemap deliberately does not support this attribute (XA4251). [Register ("my/app/HandWrittenNativeRegistrationPeer", DoNotGenerateAcw = true)] @@ -350,6 +380,7 @@ public class CustomView : Android.Views.View protected CustomView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } [Register ("", "()V", "")] + [JavaAnnotation] public CustomView () : base (default, default) { } [Register ("", "(Landroid/content/Context;)V", "")] @@ -583,6 +614,7 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public static ExportFieldExample? GetInstance () => default; [Java.Interop.ExportField ("VALUE")] + [JavaAnnotation] public string GetValue () => ""; }