From 8a28b3d9f1280f5a22d756a41a3a197845716cef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:01:27 +0100 Subject: [PATCH 1/5] [TrimmableTypeMap][Core A] Foundation model + metadata type providers --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 19 ++ .../Scanner/CustomAttributeTypeProvider.cs | 90 +++++++++ .../Scanner/JavaPeerInfo.cs | 173 ++++++++++++++++++ .../Scanner/SignatureTypeProvider.cs | 87 +++++++++ 4 files changed, 369 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj new file mode 100644 index 00000000000..ac24040ffa4 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -0,0 +1,19 @@ + + + + + $(TargetFrameworkNETStandard) + latest + enable + Nullable + Microsoft.Android.Sdk.TrimmableTypeMap + + + + + + + + + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs new file mode 100644 index 00000000000..2152e557bb8 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ICustomAttributeTypeProvider implementation for decoding +/// custom attribute values via System.Reflection.Metadata. +/// +sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider +{ + readonly MetadataReader reader; + + public CustomAttributeTypeProvider (MetadataReader reader) + { + this.reader = reader; + } + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); + + public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = metadataReader.GetTypeDefinition (handle); + var name = metadataReader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; + } + var ns = metadataReader.GetString (typeDef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = metadataReader.GetTypeReference (handle); + var name = metadataReader.GetString (typeRef.Name); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; + } + var ns = metadataReader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromSerializedName (string name) => name; + + public PrimitiveTypeCode GetUnderlyingEnumType (string type) + { + // Find the enum type in this assembly's metadata and read its value__ field type. + foreach (var typeHandle in reader.TypeDefinitions) { + var typeDef = reader.GetTypeDefinition (typeHandle); + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + var fullName = ns.Length > 0 ? ns + "." + name : name; + + if (fullName != type) + continue; + + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; + } + } + + // Default to Int32 for enums defined in other assemblies + return PrimitiveTypeCode.Int32; + } + + public string GetSystemType () => "System.Type"; + + public string GetSZArrayType (string elementType) => elementType + "[]"; + + public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs new file mode 100644 index 00000000000..5b926186d9f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Represents a Java peer type discovered during assembly scanning. +/// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). +/// Generators consume this data model — they never touch PEReader/MetadataReader. +/// +sealed class JavaPeerInfo +{ + /// + /// JNI type name, e.g., "android/app/Activity". + /// Extracted from the [Register] attribute. + /// + public string JavaName { get; set; } = ""; + + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public string CompatJniName { get; set; } = ""; + + /// + /// Full managed type name, e.g., "Android.App.Activity". + /// + public string ManagedTypeName { get; set; } = ""; + + /// + /// Assembly name the type belongs to, e.g., "Mono.Android". + /// + public string AssemblyName { get; set; } = ""; + + /// + /// JNI name of the base Java type, e.g., "android/app/Activity" for a type + /// that extends Activity. Null for java/lang/Object or types without a Java base. + /// Needed by JCW Java source generation ("extends" clause). + /// + public string? BaseJavaName { get; set; } + + /// + /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. + /// Needed by JCW Java source generation ("implements" clause). + /// + public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); + + public bool IsInterface { get; set; } + public bool IsAbstract { get; set; } + + /// + /// If true, this is a Managed Callable Wrapper (MCW) binding type. + /// No JCW or RegisterNatives will be generated for it. + /// + public bool DoNotGenerateAcw { get; set; } + + /// + /// Types with component attributes ([Activity], [Service], etc.), + /// custom views from layout XML, or manifest-declared components + /// are unconditionally preserved (not trimmable). + /// + public bool IsUnconditional { get; set; } + + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; set; } = Array.Empty (); + + /// + /// Information about the activation constructor for this type. + /// May reference a base type's constructor if the type doesn't define its own. + /// + public ActivationCtorInfo? ActivationCtor { get; set; } + + /// + /// For interfaces and abstract types, the name of the invoker type + /// used to instantiate instances from Java. + /// + public string? InvokerTypeName { get; set; } + + /// + /// True if this is an open generic type definition. + /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. + /// + public bool IsGenericDefinition { get; set; } +} + +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed class MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public string JniName { get; set; } = ""; + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public string JniSignature { get; set; } = ""; + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; set; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public string ManagedMethodName { get; set; } = ""; + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; set; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; set; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; set; } +} + +/// +/// Describes how to call the activation constructor for a Java peer type. +/// +sealed class ActivationCtorInfo +{ + /// + /// The type that declares the activation constructor. + /// May be the type itself or a base type. + /// + public string DeclaringTypeName { get; set; } = ""; + + /// + /// The assembly containing the declaring type. + /// + public string DeclaringAssemblyName { get; set; } = ""; + + /// + /// The style of activation constructor found. + /// + public ActivationCtorStyle Style { get; set; } +} + +enum ActivationCtorStyle +{ + /// + /// Xamarin.Android style: (IntPtr handle, JniHandleOwnership transfer) + /// + XamarinAndroid, + + /// + /// Java.Interop style: (ref JniObjectReference reference, JniObjectReferenceOptions options) + /// + JavaInterop, +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs new file mode 100644 index 00000000000..185f10c89bd --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ISignatureTypeProvider implementation for decoding method +/// signatures via System.Reflection.Metadata. +/// Returns fully qualified type name strings. +/// +sealed class SignatureTypeProvider : ISignatureTypeProvider +{ + public static readonly SignatureTypeProvider Instance = new (); + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch { + PrimitiveTypeCode.Void => "System.Void", + PrimitiveTypeCode.Boolean => "System.Boolean", + PrimitiveTypeCode.Char => "System.Char", + PrimitiveTypeCode.SByte => "System.SByte", + PrimitiveTypeCode.Byte => "System.Byte", + PrimitiveTypeCode.Int16 => "System.Int16", + PrimitiveTypeCode.UInt16 => "System.UInt16", + PrimitiveTypeCode.Int32 => "System.Int32", + PrimitiveTypeCode.UInt32 => "System.UInt32", + PrimitiveTypeCode.Int64 => "System.Int64", + PrimitiveTypeCode.UInt64 => "System.UInt64", + PrimitiveTypeCode.Single => "System.Single", + PrimitiveTypeCode.Double => "System.Double", + PrimitiveTypeCode.String => "System.String", + PrimitiveTypeCode.Object => "System.Object", + PrimitiveTypeCode.IntPtr => "System.IntPtr", + PrimitiveTypeCode.UIntPtr => "System.UIntPtr", + PrimitiveTypeCode.TypedReference => "System.TypedReference", + _ => typeCode.ToString (), + }; + + public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = reader.GetTypeDefinition (handle); + var ns = reader.GetString (typeDef.Namespace); + var name = reader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; + } + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference (handle); + var name = reader.GetString (typeRef.Name); + + // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; + } + + var ns = reader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var typeSpec = reader.GetTypeSpecification (handle); + return typeSpec.DecodeSignature (this, genericContext); + } + + public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]"; + public string GetByReferenceType (string elementType) => elementType + "&"; + public string GetPointerType (string elementType) => elementType + "*"; + public string GetPinnedType (string elementType) => elementType; + public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + + public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) + { + return genericType + "<" + string.Join (",", typeArguments) + ">"; + } + + public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index; + public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index; + + public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; +} From 23e6975823280783ca8a5d074b22605261632ace Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:02:20 +0100 Subject: [PATCH 2/5] [TrimmableTypeMap][Core B] Add AssemblyIndex metadata indexer --- .../Scanner/AssemblyIndex.cs | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs new file mode 100644 index 00000000000..57dc986a994 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions, +/// all subsequent lookups are O(1) dictionary lookups. +/// +sealed class AssemblyIndex : IDisposable +{ + readonly PEReader peReader; + internal readonly CustomAttributeTypeProvider customAttributeTypeProvider; + + public MetadataReader Reader { get; } + public string AssemblyName { get; } + public string FilePath { get; } + + /// + /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. + /// + public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal); + + /// + /// Cached [Register] attribute data per type. + /// + public Dictionary RegisterInfoByType { get; } = new (); + + /// + /// All custom attribute data per type, pre-parsed for the attributes we care about. + /// + public Dictionary AttributesByType { get; } = new (); + + /// + /// Type names of attributes that implement Java.Interop.IJniNameProviderAttribute + /// in this assembly. Used to detect JNI name providers without hardcoding attribute names. + /// + public HashSet JniNameProviderAttributes { get; } = new (StringComparer.Ordinal); + + /// + /// Merged set of all JNI name provider attribute type names across all loaded assemblies. + /// Set by after all assemblies are indexed. + /// + HashSet? allJniNameProviderAttributes; + + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath) + { + this.peReader = peReader; + this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); + Reader = reader; + AssemblyName = assemblyName; + FilePath = filePath; + } + + public static AssemblyIndex Create (string filePath) + { + var peReader = new PEReader (File.OpenRead (filePath)); + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); + index.Build (); + return index; + } + + void Build () + { + FindJniNameProviderAttributes (); + + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + + var fullName = GetFullName (typeDef, Reader); + if (fullName.Length == 0) { + continue; + } + + TypesByFullName [fullName] = typeHandle; + + var (registerInfo, attrInfo) = ParseAttributes (typeDef); + + if (attrInfo != null) { + AttributesByType [typeHandle] = attrInfo; + } + + if (registerInfo != null) { + RegisterInfoByType [typeHandle] = registerInfo; + } + } + } + + /// + /// Finds all types in this assembly that implement Java.Interop.IJniNameProviderAttribute. + /// + void FindJniNameProviderAttributes () + { + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + if (ImplementsIJniNameProviderAttribute (typeDef)) { + var name = Reader.GetString (typeDef.Name); + JniNameProviderAttributes.Add (name); + } + } + } + + bool ImplementsIJniNameProviderAttribute (TypeDefinition typeDef) + { + foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { + var impl = Reader.GetInterfaceImplementation (implHandle); + if (impl.Interface.Kind == HandleKind.TypeReference) { + var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); + var name = Reader.GetString (typeRef.Name); + var ns = Reader.GetString (typeRef.Namespace); + if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { + return true; + } + } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { + var ifaceTypeDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); + var name = Reader.GetString (ifaceTypeDef.Name); + var ns = Reader.GetString (ifaceTypeDef.Namespace); + if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { + return true; + } + } + } + return false; + } + + /// + /// Sets the merged set of JNI name provider attributes from all loaded assemblies + /// and re-classifies any attributes that weren't recognized in the initial pass. + /// + public void ReclassifyAttributes (HashSet mergedJniNameProviders) + { + allJniNameProviderAttributes = mergedJniNameProviders; + + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + + // Skip types that already have component attribute info + if (AttributesByType.TryGetValue (typeHandle, out var existing) && existing.HasComponentAttribute) { + continue; + } + + // Re-check custom attributes with the full set of known providers + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = Reader.GetCustomAttribute (caHandle); + var attrName = GetCustomAttributeName (ca, Reader); + + if (attrName == null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") { + continue; + } + + if (mergedJniNameProviders.Contains (attrName) && !IsKnownComponentAttribute (attrName)) { + var componentName = TryGetNameProperty (ca); + if (componentName != null) { + var attrInfo = existing ?? new TypeAttributeInfo (); + attrInfo.HasComponentAttribute = true; + attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); + AttributesByType [typeHandle] = attrInfo; + } + } + } + } + } + + internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader) + { + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + + if (typeDef.IsNested) { + var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); + var parentName = GetFullName (declaringType, reader); + return parentName + "+" + name; + } + + if (ns.Length == 0) { + return name; + } + + return ns + "." + name; + } + + (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) + { + RegisterInfo? registerInfo = null; + TypeAttributeInfo? attrInfo = null; + + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = Reader.GetCustomAttribute (caHandle); + var attrName = GetCustomAttributeName (ca, Reader); + + if (attrName == null) { + continue; + } + + if (attrName == "RegisterAttribute") { + registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); + } else if (attrName == "ExportAttribute") { + // [Export] methods are detected per-method in CollectMarshalMethods + } else if (IsJniNameProviderAttribute (attrName)) { + attrInfo ??= new TypeAttributeInfo (); + attrInfo.HasComponentAttribute = true; + var componentName = TryGetNameProperty (ca); + if (componentName != null) { + attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); + } + if (attrName == "ApplicationAttribute") { + attrInfo.ApplicationBackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + attrInfo.ApplicationManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); + } + } + } + + return (registerInfo, attrInfo); + } + + /// + /// Checks if an attribute type name is a known IJniNameProviderAttribute implementor. + /// Uses the local set first (from this assembly), then falls back to the merged set + /// (populated after all assemblies are loaded), then falls back to hardcoded names + /// for the well-known Android component attributes. + /// + bool IsJniNameProviderAttribute (string attrName) + { + if (JniNameProviderAttributes.Contains (attrName)) { + return true; + } + + if (allJniNameProviderAttributes != null && allJniNameProviderAttributes.Contains (attrName)) { + return true; + } + + // Fallback for the case where we haven't loaded the assembly defining the attribute yet. + // This covers the common case where user assemblies reference Mono.Android attributes. + return IsKnownComponentAttribute (attrName); + } + + static bool IsKnownComponentAttribute (string attrName) + { + return attrName == "ActivityAttribute" + || attrName == "ServiceAttribute" + || attrName == "BroadcastReceiverAttribute" + || attrName == "ContentProviderAttribute" + || attrName == "ApplicationAttribute" + || attrName == "InstrumentationAttribute"; + } + + internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) + { + if (ca.Constructor.Kind == HandleKind.MemberReference) { + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeReference) { + var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent); + return reader.GetString (typeRef.Name); + } + } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) { + var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); + var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + return reader.GetString (declaringType.Name); + } + return null; + } + + internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider) + { + var value = ca.DecodeValue (provider); + + string jniName = ""; + string? signature = null; + string? connector = null; + bool doNotGenerateAcw = false; + + if (value.FixedArguments.Length > 0) { + jniName = (string?)value.FixedArguments [0].Value ?? ""; + } + if (value.FixedArguments.Length > 1) { + signature = (string?)value.FixedArguments [1].Value; + } + if (value.FixedArguments.Length > 2) { + connector = (string?)value.FixedArguments [2].Value; + } + + if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { + doNotGenerateAcw = doNotGenerateAcwValue; + } + + return new RegisterInfo (jniName, signature, connector, doNotGenerateAcw); + } + + string? TryGetTypeProperty (CustomAttribute ca, string propertyName) + { + var value = ca.DecodeValue (customAttributeTypeProvider); + var typeName = TryGetNamedStringArgument (value, propertyName); + if (!string.IsNullOrEmpty (typeName)) { + return typeName; + } + return null; + } + + string? TryGetNameProperty (CustomAttribute ca) + { + var value = ca.DecodeValue (customAttributeTypeProvider); + + // Check named arguments first (e.g., [Activity(Name = "...")]) + var name = TryGetNamedStringArgument (value, "Name"); + if (!string.IsNullOrEmpty (name)) { + return name; + } + + // Fall back to first constructor argument (e.g., [CustomJniName("...")]) + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) { + return ctorName; + } + + return null; + } + + static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue) + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is bool boolValue) { + argumentValue = boolValue; + return true; + } + } + + argumentValue = false; + return false; + } + + static string? TryGetNamedStringArgument (CustomAttributeValue value, string argumentName) + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is string stringValue) { + return stringValue; + } + } + + return null; + } + + public void Dispose () + { + peReader.Dispose (); + } +} + +/// +/// Parsed [Register] or [Export] attribute data for a type or method. +/// +sealed class RegisterInfo +{ + public string JniName { get; } + public string? Signature { get; } + public string? Connector { get; } + public bool DoNotGenerateAcw { get; } + + /// + /// For [Export] methods: Java exception type names the method declares it can throw. + /// + public IReadOnlyList? ThrownNames { get; } + + /// + /// For [Export] methods: super constructor arguments string. + /// + public string? SuperArgumentsString { get; } + + public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw, + IReadOnlyList? thrownNames = null, string? superArgumentsString = null) + { + JniName = jniName; + Signature = signature; + Connector = connector; + DoNotGenerateAcw = doNotGenerateAcw; + ThrownNames = thrownNames; + SuperArgumentsString = superArgumentsString; + } +} + +/// +/// Aggregated attribute information for a type, beyond [Register]. +/// +sealed class TypeAttributeInfo +{ + /// + /// Type has [Activity], [Service], [BroadcastReceiver], [ContentProvider], + /// [Application], or [Instrumentation]. + /// + public bool HasComponentAttribute { get; set; } + + /// + /// The JNI name from the Name property of a component attribute + /// (e.g., [Activity(Name = "my.app.MainActivity")] → "my/app/MainActivity"). + /// Null if no Name was specified on the component attribute. + /// + public string? ComponentAttributeJniName { get; set; } + + /// + /// If the type has [Application(BackupAgent = typeof(X))], + /// this is the full name of X. + /// + public string? ApplicationBackupAgent { get; set; } + + /// + /// If the type has [Application(ManageSpaceActivity = typeof(X))], + /// this is the full name of X. + /// + public string? ApplicationManageSpaceActivity { get; set; } +} From 177d0e2e2d0f72c8fb3fa8434eb06515cd2b5639 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:02:24 +0100 Subject: [PATCH 3/5] [TrimmableTypeMap][Core C] Add JavaPeerScanner execution logic --- .../Scanner/JavaPeerScanner.cs | 718 ++++++++++++++++++ 1 file changed, 718 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs new file mode 100644 index 00000000000..fe15dfb159b --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Scans assemblies for Java peer types using System.Reflection.Metadata. +/// Two-phase architecture: +/// Phase 1: Build per-assembly indices (fast, O(1) lookups) +/// Phase 2: Analyze types using cached indices +/// +sealed class JavaPeerScanner : IDisposable +{ + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); + readonly Dictionary activationCtorCache = new (StringComparer.Ordinal); + + /// + /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. + /// Checks the specified assembly (by name) in the assembly cache. + /// + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex) + { + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) && + resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { + return true; + } + handle = default; + resolvedIndex = null!; + return false; + } + + /// + /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling + /// nested types whose ResolutionScope is another TypeReference. + /// + static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index) + { + var typeRef = index.Reader.GetTypeReference (handle); + var name = index.Reader.GetString (typeRef.Name); + var ns = index.Reader.GetString (typeRef.Namespace); + + var scope = typeRef.ResolutionScope; + switch (scope.Kind) { + case HandleKind.AssemblyReference: { + var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); + var fullName = ns.Length > 0 ? ns + "." + name : name; + return (fullName, index.Reader.GetString (asmRef.Name)); + } + case HandleKind.TypeReference: { + // Nested type: recurse to get the declaring type's full name and assembly + var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); + return (parentFullName + "+" + name, assemblyName); + } + default: { + var fullName = ns.Length > 0 ? ns + "." + name : name; + return (fullName, index.AssemblyName); + } + } + } + + /// + /// Looks up the [Register] JNI name for a type identified by name + assembly. + /// + string? ResolveRegisterJniName (string typeName, string assemblyName) + { + if (TryResolveType (typeName, assemblyName, out var handle, out var resolvedIndex) && + resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo)) { + return regInfo.JniName; + } + return null; + } + + /// + /// Phase 1: Build indices for all assemblies. + /// Phase 2: Scan all types and produce JavaPeerInfo records. + /// + public List Scan (IReadOnlyList assemblyPaths) + { + // Phase 1: Build indices for all assemblies + foreach (var path in assemblyPaths) { + var index = AssemblyIndex.Create (path); + assemblyCache [index.AssemblyName] = index; + } + + // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies + // and re-classify any attributes that weren't recognized in the initial pass + // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). + var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); + foreach (var index in assemblyCache.Values) { + mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); + } + foreach (var index in assemblyCache.Values) { + index.ReclassifyAttributes (mergedJniNameProviders); + } + + // Phase 2: Analyze types using cached indices + var resultsByManagedName = new Dictionary (StringComparer.Ordinal); + + foreach (var index in assemblyCache.Values) { + ScanAssembly (index, resultsByManagedName); + } + + // Phase 3: Force unconditional on types referenced by [Application] attributes + ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); + + return new List (resultsByManagedName.Values); + } + + /// + /// Types referenced by [Application(BackupAgent = typeof(X))] or + /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, + /// because the manifest will reference them even if nothing else does. + /// + static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) + { + foreach (var index in assemblyCache.Values) { + foreach (var attrInfo in index.AttributesByType.Values) { + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); + } + } + } + + static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) + { + if (managedTypeName == null) { + return; + } + + // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." + // Strip to just the type name for lookup + var commaIndex = managedTypeName.IndexOf (','); + if (commaIndex > 0) { + managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); + } + + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + peer.IsUnconditional = true; + } + } + + void ScanAssembly (AssemblyIndex index, Dictionary results) + { + foreach (var typeHandle in index.Reader.TypeDefinitions) { + var typeDef = index.Reader.GetTypeDefinition (typeHandle); + + // Skip module-level types + if (index.Reader.GetString (typeDef.Name) == "") { + continue; + } + + // Determine the JNI name and whether this is a known Java peer. + // Priority: + // 1. [Register] attribute → use JNI name from attribute + // 2. Component attribute Name property → convert dots to slashes + // 3. Extends a known Java peer → auto-compute JNI name via CRC64 + // 4. None of the above → not a Java peer, skip + string? jniName = null; + string? compatJniName = null; + bool doNotGenerateAcw = false; + + index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); + index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); + + if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) { + jniName = registerInfo.JniName; + compatJniName = jniName; + doNotGenerateAcw = registerInfo.DoNotGenerateAcw; + } else if (attrInfo?.ComponentAttributeJniName != null) { + // User type with [Activity(Name = "...")] but no [Register] + jniName = attrInfo.ComponentAttributeJniName; + compatJniName = jniName; + } else { + // No explicit JNI name — check if this type extends a known Java peer. + // If so, auto-compute JNI name from the managed type name via CRC64. + if (ExtendsJavaPeer (typeDef, index)) { + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); + } else { + continue; + } + } + + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); + + var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; + var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; + var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; + + var isUnconditional = attrInfo?.HasComponentAttribute ?? false; + string? invokerTypeName = null; + + // Resolve base Java type name + var baseJavaName = ResolveBaseJavaName (typeDef, index, results); + + // Resolve implemented Java interface names + var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); + + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + + // Resolve activation constructor + var activationCtor = ResolveActivationCtor (fullName, typeDef, index); + + // For interfaces/abstract types, try to find invoker type name + if (isInterface || isAbstract) { + invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); + } + + var peer = new JavaPeerInfo { + JavaName = jniName, + CompatJniName = compatJniName, + ManagedTypeName = fullName, + AssemblyName = index.AssemblyName, + BaseJavaName = baseJavaName, + ImplementedInterfaceJavaNames = implementedInterfaces, + IsInterface = isInterface, + IsAbstract = isAbstract, + DoNotGenerateAcw = doNotGenerateAcw, + IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, + ActivationCtor = activationCtor, + InvokerTypeName = invokerTypeName, + IsGenericDefinition = isGenericDefinition, + }; + + results [fullName] = peer; + } + } + + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + var registerInfo = TryGetMethodRegisterInfo (methodDef, index); + if (registerInfo == null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister == null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature == null && registerInfo.Connector == null) { + return; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + ThrownNames = registerInfo.ThrownNames, + SuperArgumentsString = registerInfo.SuperArgumentsString, + }); + } + + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo == null) { + return null; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + // First try [Register] attribute + var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); + if (registerJniName != null) { + return registerJniName; + } + + // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + if (results.TryGetValue (baseTypeName, out var basePeer)) { + return basePeer.JavaName; + } + + return null; + } + + List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index) + { + var result = new List (); + var interfaceImpls = typeDef.GetInterfaceImplementations (); + + foreach (var implHandle in interfaceImpls) { + var impl = index.Reader.GetInterfaceImplementation (implHandle); + var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); + if (ifaceJniName != null) { + result.Add (ifaceJniName); + } + } + + return result; + } + + string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) + { + var resolved = ResolveEntityHandle (interfaceHandle, index); + return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + } + + static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + { + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + } + + if (attrName == "ExportAttribute") { + return ParseExportAttribute (ca, methodDef, index); + } + } + return null; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + } + } + return null; + } + + static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = ca.DecodeValue (index.customAttributeTypeProvider); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is string[] names) { + thrownNames = new List (names); + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (exportName == null || exportName.Length == 0) { + exportName = index.Reader.GetString (methodDef.Name); + } + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return new RegisterInfo (exportName, jniSig, null, false, + thrownNames: thrownNames, superArgumentsString: superArguments); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); + } + return "Ljava/lang/Object;"; + } + } + + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) + { + if (activationCtorCache.TryGetValue (typeName, out var cached)) { + return cached; + } + + // Check this type's constructors + var ownCtor = FindActivationCtorOnType (typeDef, index); + if (ownCtor != null) { + var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; + activationCtorCache [typeName] = info; + return info; + } + + // Walk base type hierarchy + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo != null) { + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); + if (result != null) { + activationCtorCache [typeName] = result; + } + return result; + } + } + + return null; + } + + static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var method = index.Reader.GetMethodDefinition (methodHandle); + var name = index.Reader.GetString (method.Name); + + if (name != ".ctor") { + continue; + } + + var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + + // XI style: (IntPtr, JniHandleOwnership) + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0] == "System.IntPtr" && + sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") { + return ActivationCtorStyle.XamarinAndroid; + } + + // JI style: (ref JniObjectReference, JniObjectReferenceOptions) + if (sig.ParameterTypes.Length == 2 && + (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") && + sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") { + return ActivationCtorStyle.JavaInterop; + } + } + + return null; + } + + /// + /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying + /// type's (fullName, assemblyName) by reading the raw signature blob. + /// + static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) + { + var typeSpec = index.Reader.GetTypeSpecification (specHandle); + var blobReader = index.Reader.GetBlobReader (typeSpec.Signature); + + // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args... + var elementType = blobReader.ReadByte (); + if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST + return null; + } + + var classOrValueType = blobReader.ReadByte (); + if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE + return null; + } + + // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec) + var codedToken = blobReader.ReadCompressedInteger (); + var tag = codedToken & 0x3; + var row = codedToken >> 2; + + switch (tag) { + case 0: { // TypeDef + var handle = MetadataTokens.TypeDefinitionHandle (row); + var baseDef = index.Reader.GetTypeDefinition (handle); + return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName); + } + case 1: // TypeRef + return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); + default: + return null; + } + } + + /// + /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName). + /// Shared by base type resolution, interface resolution, and any handle-to-name lookup. + /// + (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) + { + switch (handle.Kind) { + case HandleKind.TypeDefinition: { + var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); + return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName); + } + case HandleKind.TypeReference: + return ResolveTypeReference ((TypeReferenceHandle)handle, index); + case HandleKind.TypeSpecification: + return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index); + default: + return null; + } + } + + (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index) + { + return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); + } + + string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) + { + // First, check the [Register] attribute's connector arg (3rd arg). + // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] + // where the connector contains the assembly-qualified invoker type name. + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) { + var connector = registerInfo.Connector; + // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." + // We want just the type name (before the first comma, if any) + var commaIndex = connector.IndexOf (','); + if (commaIndex > 0) { + return connector.Substring (0, commaIndex).Trim (); + } + if (connector.Length > 0) { + return connector; + } + } + + // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" + var invokerName = typeName + "Invoker"; + if (index.TypesByFullName.ContainsKey (invokerName)) { + return invokerName; + } + return null; + } + + public void Dispose () + { + foreach (var index in assemblyCache.Values) { + index.Dispose (); + } + assemblyCache.Clear (); + } + + readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + + /// + /// Check if a type extends a known Java peer (has [Register] or component attribute) + /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles. + /// + bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) + { + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); + var key = index.AssemblyName + ":" + fullName; + + if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { + return cached; + } + + // Mark as false to prevent cycles, then compute + extendsJavaPeerCache [key] = false; + + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo == null) { + return false; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + return false; + } + + // Direct hit: base has [Register] or component attribute + if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) { + extendsJavaPeerCache [key] = true; + return true; + } + if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { + extendsJavaPeerCache [key] = true; + return true; + } + + // Recurse up the hierarchy + var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ExtendsJavaPeer (baseDef, baseIndex); + extendsJavaPeerCache [key] = result; + return result; + } + + /// + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. + /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. + /// Generic backticks are replaced with _. + /// + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + { + var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); + + if (parentJniName != null) { + var name = parentJniName + "_" + typeName; + return (name, name); + } + + var packageName = GetCrc64PackageName (ns, index.AssemblyName); + var jniName = packageName + "/" + typeName; + + string compatName = ns.Length == 0 + ? typeName + : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName; + + return (jniName, compatName); + } + + /// + /// Builds the type name part (handling nesting) and returns either a parent's + /// registered JNI name or the outermost namespace. + /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types + /// and if a parent has [Register] or a component attribute JNI name, uses that + /// as prefix instead of computing CRC64 from the namespace. + /// + static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) + { + var firstName = index.Reader.GetString (typeDef.Name).Replace ('`', '_'); + + // Fast path: non-nested types (the vast majority) + if (!typeDef.IsNested) { + return (firstName, null, index.Reader.GetString (typeDef.Namespace)); + } + + // Nested type: walk up declaring types, collecting name parts + var nameParts = new List (4) { firstName }; + var current = typeDef; + string? parentJniName = null; + + do { + var parentHandle = current.GetDeclaringType (); + current = index.Reader.GetTypeDefinition (parentHandle); + + // Check if the parent has a registered JNI name + if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !string.IsNullOrEmpty (parentRegister.JniName)) { + parentJniName = parentRegister.JniName; + break; + } + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) { + parentJniName = parentAttr.ComponentAttributeJniName; + break; + } + + nameParts.Add (index.Reader.GetString (current.Name).Replace ('`', '_')); + } while (current.IsNested); + + nameParts.Reverse (); + var typeName = string.Join ("_", nameParts); + var ns = index.Reader.GetString (current.Namespace); + + return (typeName, parentJniName, ns); + } + + static string GetCrc64PackageName (string ns, string assemblyName) + { + // Only Mono.Android preserves the namespace directly + if (assemblyName == "Mono.Android") { + return ns.ToLowerInvariant ().Replace ('.', '/'); + } + + var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName); + var hash = System.IO.Hashing.Crc64.Hash (data); + return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + } +} From 5dc8a663e352dc217646f3eff81ac821510bc019 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:30:49 +0100 Subject: [PATCH 4/5] [TrimmableTypeMap] Foundational scanner unit tests slice --- ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 38 ++ .../Scanner/JavaPeerScannerTests.cs | 268 ++++++++ .../TestFixtures/StubAttributes.cs | 143 ++++ .../TestFixtures/TestFixtures.csproj | 13 + .../TestFixtures/TestTypes.cs | 628 ++++++++++++++++++ 5 files changed, 1090 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs 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 new file mode 100644 index 00000000000..6370a77e680 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.Tests + + + + + + + + + + + + + + + + + + false + + + + + + + <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs new file mode 100644 index 00000000000..34e5976b198 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + JavaPeerInfo FindByJavaName (List peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + JavaPeerInfo FindByManagedName (List peers, string managedName) + { + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + [Fact] + public void Scan_FindsAllJavaPeerTypes () + { + var peers = ScanFixtures (); + Assert.NotEmpty (peers); + // MCW types with [Register] + Assert.Contains (peers, p => p.JavaName == "java/lang/Object"); + Assert.Contains (peers, p => p.JavaName == "android/app/Activity"); + // User type with JNI name from [Activity(Name="...")] + Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity"); + // Exception/Throwable hierarchy + Assert.Contains (peers, p => p.JavaName == "java/lang/Throwable"); + Assert.Contains (peers, p => p.JavaName == "java/lang/Exception"); + } + + [Fact] + public void Scan_McwTypes_HaveDoNotGenerateAcw () + { + var peers = ScanFixtures (); + + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.True (activity.DoNotGenerateAcw, "Activity should be MCW (DoNotGenerateAcw=true)"); + + var button = FindByJavaName (peers, "android/widget/Button"); + Assert.True (button.DoNotGenerateAcw, "Button should be MCW"); + } + + [Fact] + public void Scan_UserTypes_DoNotGenerateAcwIsFalse () + { + var peers = ScanFixtures (); + + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw, "MainActivity should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_ActivityType_IsUnconditional () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.True (mainActivity.IsUnconditional, "MainActivity with [Activity] should be unconditional"); + } + + [Fact] + public void Scan_ServiceType_IsUnconditional () + { + var peers = ScanFixtures (); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.True (service.IsUnconditional, "MyService with [Service] should be unconditional"); + } + + [Fact] + public void Scan_BroadcastReceiverType_IsUnconditional () + { + var peers = ScanFixtures (); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.True (receiver.IsUnconditional, "MyReceiver with [BroadcastReceiver] should be unconditional"); + } + + [Fact] + public void Scan_ContentProviderType_IsUnconditional () + { + var peers = ScanFixtures (); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.True (provider.IsUnconditional, "MyProvider with [ContentProvider] should be unconditional"); + } + + [Fact] + public void Scan_TypeWithoutComponentAttribute_IsTrimmable () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "MyHelper without component attr should be trimmable"); + } + + [Fact] + public void Scan_McwBinding_IsTrimmable () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.False (activity.IsUnconditional, "MCW Activity should be trimmable (no component attr on MCW type)"); + } + + [Fact] + public void Scan_InterfaceType_IsMarkedAsInterface () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.True (listener.IsInterface, "IOnClickListener should be marked as interface"); + } + + [Fact] + public void Scan_InvokerTypes_AreIncluded () + { + var peers = ScanFixtures (); + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw, "Invoker should have DoNotGenerateAcw=true"); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_GenericType_IsGenericDefinition () + { + var peers = ScanFixtures (); + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition, "GenericHolder should be marked as generic definition"); + } + + [Fact] + public void Scan_AbstractType_IsMarkedAbstract () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + Assert.True (abstractBase.IsAbstract, "AbstractBase should be marked as abstract"); + } + + [Fact] + public void Scan_MarshalMethods_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotEmpty (activity.MarshalMethods); + } + + [Fact] + public void Scan_UserTypeOverride_CollectsMarshalMethods () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotEmpty (mainActivity.MarshalMethods); + + var onCreate = mainActivity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_ResolvesToSelf () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotNull (activity.ActivationCtor); + Assert.Equal ("Android.App.Activity", activity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, activity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithoutOwnActivationCtor_InheritsFromBase () + { + var peers = ScanFixtures (); + var simpleActivity = FindByJavaName (peers, "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", simpleActivity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, simpleActivity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_DoesNotLookAtBase () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotNull (mainActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", mainActivity.ActivationCtor.DeclaringTypeName); + } + + [Fact] + public void Scan_AllTypes_HaveAssemblyName () + { + var peers = ScanFixtures (); + Assert.All (peers, peer => + Assert.False (string.IsNullOrEmpty (peer.AssemblyName), + $"Type {peer.ManagedTypeName} should have assembly name")); + } + + [Fact] + public void Scan_InvokerSharesJavaNameWithInterface () + { + var peers = ScanFixtures (); + var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + // Interface + Invoker share the same JNI name (this is expected — they're aliases) + Assert.Equal (2, clickListenerPeers.Count); + Assert.Contains (clickListenerPeers, p => p.IsInterface); + Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + } + + [Fact] + public void Scan_ActivityBaseJavaName_IsJavaLangObject () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal ("java/lang/Object", activity.BaseJavaName); + } + + [Fact] + public void Scan_MainActivityBaseJavaName_IsActivity () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal ("android/app/Activity", mainActivity.BaseJavaName); + } + + [Fact] + public void Scan_JavaLangObjectBaseJavaName_IsNull () + { + var peers = ScanFixtures (); + var jlo = FindByJavaName (peers, "java/lang/Object"); + Assert.Null (jlo.BaseJavaName); + } + + [Fact] + public void Scan_TypeImplementingInterface_HasInterfaceJavaNames () + { + var peers = ScanFixtures (); + var clickable = FindByJavaName (peers, "my/app/ClickableView"); + Assert.Contains ("android/view/View$OnClickListener", clickable.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_TypeNotImplementingInterface_HasEmptyList () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.Empty (helper.ImplementedInterfaceJavaNames); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs new file mode 100644 index 00000000000..fcd4519d277 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -0,0 +1,143 @@ +// Minimal stub attributes mirroring the real Mono.Android attributes. +// These exist solely so the test fixture assembly can have types +// with the same attribute shapes the scanner expects. + +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, + TransferLocalRef = 1, + TransferGlobalRef = 2, + } +} + +namespace Android.App +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class ActivityAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public bool MainLauncher { get; set; } + public string? Label { get; set; } + public string? Icon { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ServiceAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class InstrumentationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ApplicationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public Type? BackupAgent { get; set; } + public Type? ManageSpaceActivity { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } +} + +namespace Android.Content +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class BroadcastReceiverAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string []? Authorities { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + + public ContentProviderAttribute (string [] authorities) + { + Authorities = authorities; + } + } +} + +namespace Java.Interop +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportAttribute : Attribute + { + public string? Name { get; set; } + + public ExportAttribute () + { + } + + public ExportAttribute (string name) + { + Name = name; + } + } +} + +namespace MyApp +{ + /// + /// Custom attribute implementing IJniNameProviderAttribute — the scanner + /// should detect this dynamically via interface resolution, not hardcoded names. + /// + [AttributeUsage (AttributeTargets.Class)] + public sealed class CustomJniNameAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + + public CustomJniNameAttribute (string name) + { + Name = name; + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj new file mode 100644 index 00000000000..f7f4c72139b --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + Microsoft.Android.Sdk.TrimmableTypeMap.Tests.TestFixtures + + false + true + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs new file mode 100644 index 00000000000..e2b909e2317 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -0,0 +1,628 @@ +// Test fixture types that exercise all scanner code paths. +// Each type is annotated with comments explaining which classification +// and behavior the scanner should produce. + +using System; +using Android.App; +using Android.Content; +using Android.Runtime; + +namespace Java.Lang +{ + [Register ("java/lang/Object", DoNotGenerateAcw = true)] + public class Object + { + public Object () + { + } + + protected Object (IntPtr handle, JniHandleOwnership transfer) + { + } + } +} + +namespace Java.Lang +{ + [Register ("java/lang/Throwable", DoNotGenerateAcw = true)] + public class Throwable : Java.Lang.Object + { + protected Throwable (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")] + public virtual string? Message { get; } + } + + [Register ("java/lang/Exception", DoNotGenerateAcw = true)] + public class Exception : Throwable + { + protected Exception (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.App +{ + [Register ("android/app/Activity", DoNotGenerateAcw = true)] + public class Activity : Java.Lang.Object + { + public Activity () + { + } + + protected Activity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual void OnCreate (/* Bundle? */ object? savedInstanceState) + { + } + + [Register ("onStart", "()V", "")] + protected virtual void OnStart () + { + } + } + + [Register ("android/app/Service", DoNotGenerateAcw = true)] + public class Service : Java.Lang.Object + { + protected Service (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Content +{ + [Register ("android/content/Context", DoNotGenerateAcw = true)] + public class Context : Java.Lang.Object + { + protected Context (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View", DoNotGenerateAcw = true)] + public class View : Java.Lang.Object + { + protected View (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Widget +{ + [Register ("android/widget/Button", DoNotGenerateAcw = true)] + public class Button : Android.Views.View + { + protected Button (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("android/widget/TextView", DoNotGenerateAcw = true)] + public class TextView : Android.Views.View + { + protected TextView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] + public interface IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] + void OnClick (View v); + } + + // Invoker types ARE internal implementation details. + // In real Mono.Android.dll, invokers DO have [Register] with DoNotGenerateAcw=true + // and the SAME JNI name as their interface. + // The scanner includes them — generators filter them later. + [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] + internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + public void OnClick (View v) + { + } + } +} + +namespace MyApp +{ + // User types get their JNI name from [Activity(Name = "...")] + // NOT from [Register] — that's only on MCW binding types. + [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] + public class MainActivity : Android.App.Activity + { + public MainActivity () + { + } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected override void OnCreate (object? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + } + + // User type without component attribute: TRIMMABLE + [Register ("my/app/MyHelper")] + public class MyHelper : Java.Lang.Object + { + [Register ("doSomething", "()V", "GetDoSomethingHandler")] + public virtual void DoSomething () + { + } + } + + // User service: UNCONDITIONAL — gets JNI name from [Service(Name = "...")] + [Service (Name = "my.app.MyService")] + public class MyService : Android.App.Service + { + protected MyService (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // User broadcast receiver: UNCONDITIONAL — gets JNI name from [BroadcastReceiver(Name = "...")] + [BroadcastReceiver (Name = "my.app.MyReceiver")] + public class MyReceiver : Java.Lang.Object + { + } + + // User content provider: UNCONDITIONAL — gets JNI name from [ContentProvider(Name = "...")] + [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider")] + public class MyProvider : Java.Lang.Object + { + } +} + +namespace MyApp.Generic +{ + [Register ("my/app/GenericHolder")] + public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + { + [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] + public virtual T? GetItem () + { + return default; + } + } +} + +namespace MyApp +{ + [Register ("my/app/AbstractBase")] + public abstract class AbstractBase : Java.Lang.Object + { + protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("doWork", "()V", "")] + public abstract void DoWork (); + } +} + +namespace MyApp +{ + [Register ("my/app/SimpleActivity")] + public class SimpleActivity : Android.App.Activity + { + // No (IntPtr, JniHandleOwnership) ctor — scanner should + // resolve to Activity's activation ctor + } +} + +namespace MyApp +{ + [Register ("my/app/ClickableView")] + public class ClickableView : Android.Views.View, Android.Views.IOnClickListener + { + protected ClickableView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("", "()V", "")] + public CustomView () + : base (default!, default) + { + } + + [Register ("", "(Landroid/content/Context;)V", "")] + public CustomView (Context context) + : base (default!, default) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/Outer")] + public class Outer : Java.Lang.Object + { + [Register ("my/app/Outer$Inner")] + public class Inner : Java.Lang.Object + { + protected Inner (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + } +} + +namespace MyApp +{ + [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")] + public interface ICallback + { + [Register ("my/app/ICallback$Result")] + public class Result : Java.Lang.Object + { + protected Result (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + } +} + +namespace MyApp +{ + [Register ("my/app/TouchHandler")] + public class TouchHandler : Java.Lang.Object + { + // bool return type (non-blittable, needs byte wrapper in UCO) + [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")] + public virtual bool OnTouch (Android.Views.View v, int action) + { + return false; + } + + // bool parameter (non-blittable) + [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")] + public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) + { + } + + // Multiple params of different JNI types + [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")] + public virtual void OnScroll (int x, float y, long timestamp, double velocity) + { + } + + // Object return type + [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")] + public virtual string? GetText () + { + return null; + } + + // Array parameter + [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")] + public virtual void SetItems (string[]? items) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/ExportExample")] + public class ExportExample : Java.Lang.Object + { + [Java.Interop.Export ("myExportedMethod")] + public void MyExportedMethod () + { + } + } +} + +namespace Android.App.Backup +{ + [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] + public class BackupAgent : Java.Lang.Object + { + protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace MyApp +{ + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] + public class MyApplication : Java.Lang.Object + { + } + + [Instrumentation (Name = "my.app.MyInstrumentation")] + public class MyInstrumentation : Java.Lang.Object + { + } + + // BackupAgent without a component attribute — would normally be trimmable, + // but [Application(BackupAgent = typeof(...))] should force it unconditional. + [Register ("my/app/MyBackupAgent")] + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // Activity without [Activity] attribute — would normally be trimmable, + // but [Application(ManageSpaceActivity = typeof(...))] should force it unconditional. + [Register ("my/app/MyManageSpaceActivity")] + public class MyManageSpaceActivity : Android.App.Activity + { + protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // User type WITHOUT [Register] — gets CRC64-computed JNI name. + // CompatJniName should use raw namespace instead of CRC64. + public class UnregisteredHelper : Java.Lang.Object + { + } +} + +namespace MyApp +{ + [Register ("my/app/MyButton")] + public class MyButton : Android.Widget.Button + { + protected MyButton (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] + public interface IOnLongClickListener + { + [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] + bool OnLongClick (View v); + } +} + +namespace MyApp +{ + [Register ("my/app/MultiInterfaceView")] + public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener + { + protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + + [Register ("onLongClick", "(Landroid/view/View;)Z", "")] + public bool OnLongClick (Android.Views.View v) { return false; } + } + + // User type with a custom IJniNameProviderAttribute — the scanner + // should detect this via interface resolution, not hardcoded attribute names. + [CustomJniName ("com.example.CustomWidget")] + public class CustomWidget : Java.Lang.Object + { + } +} + +// ================================================================ +// Edge case: generic base type (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] + public class GenericBase : Java.Lang.Object where T : class + { + protected GenericBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("my/app/ConcreteFromGeneric")] + public class ConcreteFromGeneric : GenericBase + { + protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// ================================================================ +// Edge case: generic interface (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/IGenericCallback", "", "")] + public interface IGenericCallback + { + } + + [Register ("my/app/GenericCallbackImpl")] + public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback + { + protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// ================================================================ +// Edge case: component-only base detection +// ================================================================ +namespace MyApp +{ + [Activity (Name = "my.app.BaseActivityNoRegister")] + public class BaseActivityNoRegister : Android.App.Activity + { + } + + public class DerivedFromComponentBase : BaseActivityNoRegister + { + } +} + +// ================================================================ +// Edge case: unregistered nested type inside [Register] parent +// ================================================================ +namespace MyApp +{ + [Register ("my/app/RegisteredParent")] + public class RegisteredParent : Java.Lang.Object + { + public class UnregisteredChild : Java.Lang.Object + { + } + } +} + +// ================================================================ +// Edge case: 3-level deep nesting +// ComputeTypeNameParts must walk multiple levels, collecting names. +// ================================================================ +namespace MyApp +{ + [Register ("my/app/DeepOuter")] + public class DeepOuter : Java.Lang.Object + { + public class Middle : Java.Lang.Object + { + public class DeepInner : Java.Lang.Object + { + } + } + } +} + +// ================================================================ +// Edge case: plain Java peer subclass — no [Register], no component attribute +// ExtendsJavaPeer must detect it via base type chain, gets CRC64 name. +// ================================================================ +namespace MyApp +{ + public class PlainActivitySubclass : Android.App.Activity + { + } +} + +// ================================================================ +// Edge case: component attribute WITHOUT Name property +// HasComponentAttribute = true but ComponentAttributeJniName = null. +// Type should still get a CRC64 JNI name (not null). +// ================================================================ +namespace MyApp +{ + [Activity (Label = "Unnamed")] + public class UnnamedActivity : Android.App.Activity + { + } +} + +// ================================================================ +// Edge case: interface implementation on unregistered type +// Type gets CRC64 JNI name but still resolves interface names. +// ================================================================ +namespace MyApp +{ + public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) + { + } + } +} + +// ================================================================ +// Edge case: [Export] method on unregistered type +// ParseExportAttribute runs on a type that gets CRC64 JNI name. +// ================================================================ +namespace MyApp +{ + public class UnregisteredExporter : Java.Lang.Object + { + [Java.Interop.Export ("doExportedWork")] + public void DoExportedWork () + { + } + } +} + +// ================================================================ +// Edge case: type in empty namespace +// ================================================================ +[Register ("my/app/GlobalType")] +public class GlobalType : Java.Lang.Object +{ + protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } +} + +public class GlobalUnregisteredType : Java.Lang.Object +{ +} From f050088209ca257702232b49b2cf8c93308883a8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:31:22 +0100 Subject: [PATCH 5/5] [TrimmableTypeMap] Add behavior and contract scanner unit tests --- .../Scanner/JavaPeerScannerTests.Behavior.cs | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs new file mode 100644 index 00000000000..f9b77c84137 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -0,0 +1,357 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Fact] + public void Scan_TypeWithRegisteredCtors_HasConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var ctors = customView.MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + } + + [Fact] + public void Scan_TypeWithoutRegisteredCtors_HasNoConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.DoesNotContain (helper.MarshalMethods, m => m.IsConstructor); + } + + [Fact] + public void Scan_MarshalMethod_JniNameIsJavaMethodName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onCreate = activity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("onCreate", onCreate.JniName); + } + + [Fact] + public void Scan_UserTypeWithActivityName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw); + Assert.True (mainActivity.IsUnconditional, "Types with [Activity] are unconditional"); + } + + [Fact] + public void Scan_UserTypeWithServiceName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.False (service.DoNotGenerateAcw); + Assert.True (service.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithBroadcastReceiverName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.False (receiver.DoNotGenerateAcw); + Assert.True (receiver.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithContentProviderName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.False (provider.DoNotGenerateAcw); + Assert.True (provider.IsUnconditional); + } + + [Fact] + public void Scan_Throwable_IsMcwType () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + Assert.True (throwable.DoNotGenerateAcw); + Assert.Equal ("java/lang/Object", throwable.BaseJavaName); + } + + [Fact] + public void Scan_Exception_ExtendsThrowable () + { + var peers = ScanFixtures (); + var exception = FindByJavaName (peers, "java/lang/Exception"); + Assert.True (exception.DoNotGenerateAcw); + Assert.Equal ("java/lang/Throwable", exception.BaseJavaName); + } + + [Fact] + public void Scan_NestedType_IsDiscovered () + { + var peers = ScanFixtures (); + var inner = FindByJavaName (peers, "my/app/Outer$Inner"); + Assert.Equal ("MyApp.Outer+Inner", inner.ManagedTypeName); + } + + [Fact] + public void Scan_NestedTypeInInterface_IsDiscovered () + { + var peers = ScanFixtures (); + var result = FindByJavaName (peers, "my/app/ICallback$Result"); + Assert.Equal ("MyApp.ICallback+Result", result.ManagedTypeName); + } + + [Fact] + public void Scan_MarshalMethod_BoolReturn_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onTouch = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onTouch"); + Assert.NotNull (onTouch); + Assert.Equal ("(Landroid/view/View;I)Z", onTouch.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_BoolParam_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onFocus = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onFocusChange"); + Assert.NotNull (onFocus); + Assert.Equal ("(Landroid/view/View;Z)V", onFocus.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_MultiplePrimitiveParams () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onScroll = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onScroll"); + Assert.NotNull (onScroll); + Assert.Equal ("(IFJD)V", onScroll.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ArrayParam () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var setItems = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "setItems"); + Assert.NotNull (setItems); + Assert.Equal ("([Ljava/lang/String;)V", setItems.JniSignature); + } + + [Fact] + public void Scan_ExportMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var export = FindByJavaName (peers, "my/app/ExportExample"); + Assert.Single (export.MarshalMethods); + var method = export.MarshalMethods [0]; + Assert.Equal ("myExportedMethod", method.JniName); + Assert.Null (method.Connector); + } + + [Fact] + public void Scan_CustomView_DiscoveredAsRegularType () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + Assert.False (customView.IsUnconditional, "Custom views are not unconditional by attribute alone"); + Assert.Equal ("android/view/View", customView.BaseJavaName); + } + + [Fact] + public void Scan_ApplicationType_IsUnconditional () + { + var peers = ScanFixtures (); + var app = FindByJavaName (peers, "my/app/MyApplication"); + Assert.True (app.IsUnconditional, "[Application] should be unconditional"); + } + + [Fact] + public void Scan_InstrumentationType_IsUnconditional () + { + var peers = ScanFixtures (); + var instr = FindByJavaName (peers, "my/app/MyInstrumentation"); + Assert.True (instr.IsUnconditional, "[Instrumentation] should be unconditional"); + } + + [Fact] + public void Scan_BackupAgent_ForcedUnconditional () + { + var peers = ScanFixtures (); + var backupAgent = FindByJavaName (peers, "my/app/MyBackupAgent"); + Assert.True (backupAgent.IsUnconditional, "BackupAgent referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_ManageSpaceActivity_ForcedUnconditional () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "my/app/MyManageSpaceActivity"); + Assert.True (activity.IsUnconditional, "ManageSpaceActivity referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_BackupAgent_NotUnconditional_WhenNotReferenced () + { + // A type extending BackupAgent that is NOT referenced from [Application] + // should remain trimmable (not unconditional). + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "Unreferenced type should remain trimmable"); + } + + [Fact] + public void Scan_McwBinding_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal (activity.JavaName, activity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithRegister_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal (mainActivity.JavaName, mainActivity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithoutRegister_CompatJniNameUsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredHelper"); + Assert.NotNull (unregistered); + + // JavaName should use CRC64 package + Assert.StartsWith ("crc64", unregistered.JavaName); + + // CompatJniName should use the raw namespace + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + var widget = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.CustomWidget"); + Assert.NotNull (widget); + + // The custom attribute provides the JNI name via IJniNameProviderAttribute + Assert.Equal ("com/example/CustomWidget", widget.JavaName); + Assert.Equal ("com/example/CustomWidget", widget.CompatJniName); + } + + [Fact] + public void Scan_DeepHierarchy_ResolvesBaseJavaName () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.Equal ("android/widget/Button", myButton.BaseJavaName); + } + + [Fact] + public void Scan_DeepHierarchy_InheritsActivationCtor () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.NotNull (myButton.ActivationCtor); + // MyButton → Button → View → Java.Lang.Object — should find XI ctor from View or Object + Assert.Equal (ActivationCtorStyle.XamarinAndroid, myButton.ActivationCtor.Style); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + } + + [Fact] + public void Scan_AbstractMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + var doWork = abstractBase.MarshalMethods.FirstOrDefault (m => m.JniName == "doWork"); + Assert.NotNull (doWork); + Assert.Equal ("()V", doWork.JniSignature); + } + + [Fact] + public void Scan_PropertyRegister_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + var getMessage = throwable.MarshalMethods.FirstOrDefault (m => m.JniName == "getMessage"); + Assert.NotNull (getMessage); + Assert.Equal ("()Ljava/lang/String;", getMessage.JniSignature); + } + + [Fact] + public void Scan_MethodWithEmptyConnector_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onStart = activity.MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + } + + [Fact] + public void Scan_InvokerWithRegisterAndDoNotGenerateAcw_IsIncluded () + { + var peers = ScanFixtures (); + // IOnClickListenerInvoker has [Register("android/view/View$OnClickListener", DoNotGenerateAcw=true)] + // It should be included in the scanner output — generators will filter it later + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_Interface_HasInvokerTypeNameFromRegisterConnector () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.NotNull (listener.InvokerTypeName); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName); + } + + [Fact] + public void Scan_Interface_IsNotMarkedDoNotGenerateAcw () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + // Interfaces have [Register("name", "", "connector")] — the 3-arg form doesn't set DoNotGenerateAcw + Assert.False (listener.DoNotGenerateAcw, "Interfaces should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_InterfaceMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + } + + [Fact] + public void Scan_GenericType_HasCorrectManagedTypeName () + { + var peers = ScanFixtures (); + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + } + +}