From 58c2b9fc9e921685b2b2fea1a69f88bf6ed0913e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:02:20 +0100 Subject: [PATCH 1/3] [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 4921d7d6f80ec100169c036f1bb49dc6352bd536 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 09:07:11 +0100 Subject: [PATCH 2/3] [TrimmableTypeMap][Core B] Apply review refactors in AssemblyIndex Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 202 +++++++++--------- 1 file changed, 105 insertions(+), 97 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 57dc986a994..b8d6c443cb3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -67,8 +67,6 @@ public static AssemblyIndex Create (string filePath) void Build () { - FindJniNameProviderAttributes (); - foreach (var typeHandle in Reader.TypeDefinitions) { var typeDef = Reader.GetTypeDefinition (typeHandle); @@ -81,53 +79,16 @@ void Build () var (registerInfo, attrInfo) = ParseAttributes (typeDef); - if (attrInfo != null) { + if (attrInfo is not null) { AttributesByType [typeHandle] = attrInfo; } - if (registerInfo != null) { + if (registerInfo is not 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. @@ -138,9 +99,10 @@ public void ReclassifyAttributes (HashSet mergedJniNameProviders) foreach (var typeHandle in Reader.TypeDefinitions) { var typeDef = Reader.GetTypeDefinition (typeHandle); + AttributesByType.TryGetValue (typeHandle, out var existing); // Skip types that already have component attribute info - if (AttributesByType.TryGetValue (typeHandle, out var existing) && existing.HasComponentAttribute) { + if (existing is not null) { continue; } @@ -149,16 +111,16 @@ public void ReclassifyAttributes (HashSet mergedJniNameProviders) var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName == null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") { + if (attrName is 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 ('.', '/'); + if (componentName is not null) { + var attrInfo = new JniNameProviderAttributeInfo (attrName) { + JniName = componentName.Replace ('.', '/'), + }; AttributesByType [typeHandle] = attrInfo; } } @@ -174,14 +136,14 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade if (typeDef.IsNested) { var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); var parentName = GetFullName (declaringType, reader); - return parentName + "+" + name; + return $"{parentName}+{name}"; } if (ns.Length == 0) { return name; } - return ns + "." + name; + return $"{ns}.{name}"; } (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) @@ -193,24 +155,23 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName == null) { + if (attrName is null) { continue; } if (attrName == "RegisterAttribute") { registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); } else if (attrName == "ExportAttribute") { - // [Export] methods are detected per-method in CollectMarshalMethods + // [Export] methods are not handled yet and supporting them will be implemented later } else if (IsJniNameProviderAttribute (attrName)) { - attrInfo ??= new TypeAttributeInfo (); - attrInfo.HasComponentAttribute = true; + attrInfo ??= CreateTypeAttributeInfo (attrName); var componentName = TryGetNameProperty (ca); - if (componentName != null) { - attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); + if (componentName is not null) { + attrInfo.JniName = componentName.Replace ('.', '/'); } - if (attrName == "ApplicationAttribute") { - attrInfo.ApplicationBackupAgent = TryGetTypeProperty (ca, "BackupAgent"); - attrInfo.ApplicationManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); } } } @@ -230,7 +191,7 @@ bool IsJniNameProviderAttribute (string attrName) return true; } - if (allJniNameProviderAttributes != null && allJniNameProviderAttributes.Contains (attrName)) { + if (allJniNameProviderAttributes is not null && allJniNameProviderAttributes.Contains (attrName)) { return true; } @@ -239,6 +200,19 @@ bool IsJniNameProviderAttribute (string attrName) return IsKnownComponentAttribute (attrName); } + static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) + { + return attrName switch { + "ActivityAttribute" => new ActivityAttributeInfo (), + "ServiceAttribute" => new ServiceAttributeInfo (), + "BroadcastReceiverAttribute" => new BroadcastReceiverAttributeInfo (), + "ContentProviderAttribute" => new ContentProviderAttributeInfo (), + "ApplicationAttribute" => new ApplicationAttributeInfo (), + "InstrumentationAttribute" => new InstrumentationAttributeInfo (), + _ => new JniNameProviderAttributeInfo (attrName), + }; + } + static bool IsKnownComponentAttribute (string attrName) { return attrName == "ActivityAttribute" @@ -350,7 +324,7 @@ public void Dispose () } /// -/// Parsed [Register] or [Export] attribute data for a type or method. +/// Parsed [Register] attribute data for a type or method. /// sealed class RegisterInfo { @@ -359,55 +333,89 @@ sealed class RegisterInfo 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) + public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw) { JniName = jniName; Signature = signature; Connector = connector; DoNotGenerateAcw = doNotGenerateAcw; - ThrownNames = thrownNames; - SuperArgumentsString = superArgumentsString; } } /// -/// Aggregated attribute information for a type, beyond [Register]. +/// Parsed [Export] attribute data for a method. /// -sealed class TypeAttributeInfo +sealed class ExportInfo { - /// - /// Type has [Activity], [Service], [BroadcastReceiver], [ContentProvider], - /// [Application], or [Instrumentation]. - /// - public bool HasComponentAttribute { get; set; } + public IReadOnlyList? ThrownNames { get; } + public string? SuperArgumentsString { get; } - /// - /// 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; } + public ExportInfo (IReadOnlyList? thrownNames, string? superArgumentsString) + { + ThrownNames = thrownNames; + SuperArgumentsString = superArgumentsString; + } +} - /// - /// If the type has [Application(BackupAgent = typeof(X))], - /// this is the full name of X. - /// - public string? ApplicationBackupAgent { get; set; } +abstract class TypeAttributeInfo +{ + protected TypeAttributeInfo (string attributeName) + { + AttributeName = attributeName; + } - /// - /// If the type has [Application(ManageSpaceActivity = typeof(X))], - /// this is the full name of X. - /// - public string? ApplicationManageSpaceActivity { get; set; } + public string AttributeName { get; } + public string? JniName { get; set; } +} + +sealed class ActivityAttributeInfo : TypeAttributeInfo +{ + public ActivityAttributeInfo () : base ("ActivityAttribute") + { + } +} + +sealed class ServiceAttributeInfo : TypeAttributeInfo +{ + public ServiceAttributeInfo () : base ("ServiceAttribute") + { + } +} + +sealed class BroadcastReceiverAttributeInfo : TypeAttributeInfo +{ + public BroadcastReceiverAttributeInfo () : base ("BroadcastReceiverAttribute") + { + } +} + +sealed class ContentProviderAttributeInfo : TypeAttributeInfo +{ + public ContentProviderAttributeInfo () : base ("ContentProviderAttribute") + { + } +} + +sealed class InstrumentationAttributeInfo : TypeAttributeInfo +{ + public InstrumentationAttributeInfo () : base ("InstrumentationAttribute") + { + } +} + +sealed class JniNameProviderAttributeInfo : TypeAttributeInfo +{ + public JniNameProviderAttributeInfo (string attributeName) : base (attributeName) + { + } +} + +sealed class ApplicationAttributeInfo : TypeAttributeInfo +{ + public ApplicationAttributeInfo () : base ("ApplicationAttribute") + { + } + + public string? BackupAgent { get; set; } + public string? ManageSpaceActivity { get; set; } } From 3fe1722c4abe4f0b297c263d3173afb594eb4b8b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 15:52:57 +0100 Subject: [PATCH 3/3] [TrimmableTypeMap][Core B] Refactor AssemblyIndex - Remove IJniNameProviderAttribute discovery and ReclassifyAttributes; only support hardcoded known component attributes for now - Remove JniNameProviderAttributeInfo (unused without dynamic discovery) - Convert RegisterInfo and ExportInfo to records with required init - Use .IsNullOrEmpty() extension method instead of string.IsNullOrEmpty() - Add NullableExtensions.cs for netstandard2.0 nullable-aware string checks - Throw on unknown component attribute name instead of silently creating a JniNameProviderAttributeInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NullableExtensions.cs | 18 +++ .../Scanner/AssemblyIndex.cs | 126 +++--------------- 2 files changed, 37 insertions(+), 107 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs new file mode 100644 index 00000000000..2928bdd6b80 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class NullableExtensions +{ + // The static methods in System.String are not NRT annotated in netstandard2.0, + // so we need to add our own extension methods to make them nullable aware. + public static bool IsNullOrEmpty ([NotNullWhen (false)] this string? str) + { + return string.IsNullOrEmpty (str); + } + + public static bool IsNullOrWhiteSpace ([NotNullWhen (false)] this string? str) + { + return string.IsNullOrWhiteSpace (str); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index b8d6c443cb3..17c686a7a31 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -34,18 +34,6 @@ sealed class AssemblyIndex : IDisposable /// 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; @@ -89,45 +77,6 @@ void Build () } } - /// - /// 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); - AttributesByType.TryGetValue (typeHandle, out var existing); - - // Skip types that already have component attribute info - if (existing is not null) { - 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 is null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") { - continue; - } - - if (mergedJniNameProviders.Contains (attrName) && !IsKnownComponentAttribute (attrName)) { - var componentName = TryGetNameProperty (ca); - if (componentName is not null) { - var attrInfo = new JniNameProviderAttributeInfo (attrName) { - JniName = componentName.Replace ('.', '/'), - }; - AttributesByType [typeHandle] = attrInfo; - } - } - } - } - } - internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader) { var name = reader.GetString (typeDef.Name); @@ -163,7 +112,7 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); } else if (attrName == "ExportAttribute") { // [Export] methods are not handled yet and supporting them will be implemented later - } else if (IsJniNameProviderAttribute (attrName)) { + } else if (IsKnownComponentAttribute (attrName)) { attrInfo ??= CreateTypeAttributeInfo (attrName); var componentName = TryGetNameProperty (ca); if (componentName is not null) { @@ -179,27 +128,6 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade 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 is not 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 TypeAttributeInfo CreateTypeAttributeInfo (string attrName) { return attrName switch { @@ -209,7 +137,7 @@ static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) "ContentProviderAttribute" => new ContentProviderAttributeInfo (), "ApplicationAttribute" => new ApplicationAttributeInfo (), "InstrumentationAttribute" => new InstrumentationAttributeInfo (), - _ => new JniNameProviderAttributeInfo (attrName), + _ => throw new ArgumentException ($"Unknown component attribute: {attrName}"), }; } @@ -262,14 +190,19 @@ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustom doNotGenerateAcw = doNotGenerateAcwValue; } - return new RegisterInfo (jniName, signature, connector, doNotGenerateAcw); + return new RegisterInfo { + JniName = jniName, + Signature = signature, + Connector = connector, + DoNotGenerateAcw = doNotGenerateAcw, + }; } string? TryGetTypeProperty (CustomAttribute ca, string propertyName) { var value = ca.DecodeValue (customAttributeTypeProvider); var typeName = TryGetNamedStringArgument (value, propertyName); - if (!string.IsNullOrEmpty (typeName)) { + if (!typeName.IsNullOrEmpty ()) { return typeName; } return null; @@ -281,12 +214,12 @@ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustom // Check named arguments first (e.g., [Activity(Name = "...")]) var name = TryGetNamedStringArgument (value, "Name"); - if (!string.IsNullOrEmpty (name)) { + if (!name.IsNullOrEmpty ()) { 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)) { + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !ctorName.IsNullOrEmpty ()) { return ctorName; } @@ -326,35 +259,21 @@ public void Dispose () /// /// Parsed [Register] attribute data for a type or method. /// -sealed class RegisterInfo +sealed record RegisterInfo { - public string JniName { get; } - public string? Signature { get; } - public string? Connector { get; } - public bool DoNotGenerateAcw { get; } - - public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw) - { - JniName = jniName; - Signature = signature; - Connector = connector; - DoNotGenerateAcw = doNotGenerateAcw; - } + public required string JniName { get; init; } + public string? Signature { get; init; } + public string? Connector { get; init; } + public bool DoNotGenerateAcw { get; init; } } /// /// Parsed [Export] attribute data for a method. /// -sealed class ExportInfo +sealed record ExportInfo { - public IReadOnlyList? ThrownNames { get; } - public string? SuperArgumentsString { get; } - - public ExportInfo (IReadOnlyList? thrownNames, string? superArgumentsString) - { - ThrownNames = thrownNames; - SuperArgumentsString = superArgumentsString; - } + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } } abstract class TypeAttributeInfo @@ -403,13 +322,6 @@ public InstrumentationAttributeInfo () : base ("InstrumentationAttribute") } } -sealed class JniNameProviderAttributeInfo : TypeAttributeInfo -{ - public JniNameProviderAttributeInfo (string attributeName) : base (attributeName) - { - } -} - sealed class ApplicationAttributeInfo : TypeAttributeInfo { public ApplicationAttributeInfo () : base ("ApplicationAttribute")