From 8e35c683663a40b25574e1cda6ace92e5552ed2f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:04:40 +0200 Subject: [PATCH 01/11] Skip unresolvable trimmable typemap peers Detect Java peer types whose base or interface metadata references a missing type in the resolved assembly set and skip them instead of rooting them into the generated trimmable type map. Log XA4256 with the unresolved type and expected assembly path, and document the warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4256.md | 27 ++ .../AssemblyInput.cs | 5 + .../ITrimmableTypeMapLogger.cs | 1 + .../Scanner/AssemblyIndex.cs | 29 ++- .../Scanner/JavaPeerScanner.cs | 230 +++++++++++++++++- .../TrimmableTypeMapGenerator.cs | 4 +- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 9 + .../Tasks/GenerateTrimmableTypeMap.cs | 6 +- .../TrimmableTypeMapGeneratorTests.cs | 20 +- 11 files changed, 323 insertions(+), 18 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4256.md create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index b9e3a345395..aec23fb1279 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -223,6 +223,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4253](xa4253.md): Generated Java callable wrapper code changed: '{path}' + [XA4254](xa4254.md): Trimmable type map Java source input directory '{input}' and output directory '{output}' must be different. + [XA4255](xa4255.md): Generated trimmable type map Java source '{path}' was not found. ++ [XA4256](xa4256.md): Skipping Java peer type '{type}' from assembly '{assembly}' because referenced type '{referencedType}' from assembly '{referencedAssembly}' could not be resolved in '{path}'. This type will not be included in the trimmable type map. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4256.md b/Documentation/docs-mobile/messages/xa4256.md new file mode 100644 index 00000000000..1c51a09b4bb --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4256.md @@ -0,0 +1,27 @@ +--- +title: .NET for Android warning XA4256 +description: XA4256 warning code +ms.date: 06/25/2026 +f1_keywords: + - "XA4256" +--- + +# .NET for Android warning XA4256 + +## Example message + +``` +warning XA4256: Skipping Java peer type 'Google.Android.Material.Shadow.ShadowDrawableWrapper' from assembly 'Xamarin.Google.Android.Material' because referenced type 'AndroidX.AppCompat.Graphics.Drawable.DrawableWrapper' from assembly 'Xamarin.AndroidX.AppCompat.AppCompatResources' could not be resolved in '/home/user/.nuget/packages/xamarin.androidx.appcompat.appcompatresources/1.6.0/lib/net6.0-android31.0/Xamarin.AndroidX.AppCompat.AppCompatResources.dll'. This type will not be included in the trimmable type map. +``` + +## Issue + +The trimmable type map found a Java peer type whose managed base type or implemented interface references a type that is not present in the resolved assembly set. + +This can happen when NuGet packages in the Android binding graph were built against different versions of another binding package. The type may be unused by the app, but NativeAOT compiles a closed world and must resolve rooted managed types eagerly. + +## Solution + +If your app uses the skipped type, update the affected NuGet packages so the referenced type exists, or update to versions of the binding packages that are compatible with each other. + +If your app does not use the skipped type, no action is required. The type is omitted from the trimmable type map so NativeAOT does not fail while resolving unused stale metadata. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs new file mode 100644 index 00000000000..d742d120d8c --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs @@ -0,0 +1,5 @@ +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 8b03d89a772..87d6c70cd46 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -12,5 +12,6 @@ public interface ITrimmableTypeMapLogger void LogGeneratedJcwFilesInfo (int sourceCount); void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); + void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 64ca498c814..243f1aede78 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -17,6 +17,7 @@ sealed class AssemblyIndex : IDisposable public MetadataReader Reader { get; } public string AssemblyName { get; } + public string AssemblyPath { get; } /// /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. @@ -38,6 +39,11 @@ sealed class AssemblyIndex : IDisposable /// public Dictionary> ReferencedTypeNamesByAssembly { get; } = new (StringComparer.OrdinalIgnoreCase); + /// + /// Type-forwarded or otherwise exported types declared by this assembly. + /// + public HashSet ExportedTypeNames { get; } = new (StringComparer.Ordinal); + /// /// True iff the assembly's metadata mentions /// Java.Interop.JniAddNativeMethodRegistrationAttribute (as a @@ -48,18 +54,19 @@ sealed class AssemblyIndex : IDisposable /// public bool MayUseJniAddNativeMethodRegistrationAttribute { get; private set; } - AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName) + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string assemblyPath) { this.peReader = peReader; this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); Reader = reader; AssemblyName = assemblyName; + AssemblyPath = assemblyPath; } - public static AssemblyIndex Create (PEReader peReader, string assemblyName) + public static AssemblyIndex Create (PEReader peReader, string assemblyName, string assemblyPath = "") { var reader = peReader.GetMetadataReader (); - var index = new AssemblyIndex (peReader, reader, assemblyName); + var index = new AssemblyIndex (peReader, reader, assemblyName, assemblyPath); index.Build (); return index; } @@ -88,6 +95,11 @@ void Build () } } + foreach (var exportedTypeHandle in Reader.ExportedTypes) { + var exportedType = Reader.GetExportedType (exportedTypeHandle); + ExportedTypeNames.Add (GetExportedTypeFullName (exportedType)); + } + foreach (var typeHandle in Reader.TypeDefinitions) { var typeDef = Reader.GetTypeDefinition (typeHandle); @@ -113,6 +125,17 @@ void Build () RegisterInfoByType [typeHandle] = registerInfo; } } + + string GetExportedTypeFullName (ExportedType exportedType) + { + var name = Reader.GetString (exportedType.Name); + if (exportedType.Implementation.Kind == HandleKind.ExportedType) { + var declaringType = Reader.GetExportedType ((ExportedTypeHandle) exportedType.Implementation); + return MetadataTypeNameResolver.JoinNestedTypeName (GetExportedTypeFullName (declaringType), name); + } + var ns = Reader.GetString (exportedType.Namespace); + return MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); + } } bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen (true)] out string? assemblyName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08316ff1787..9c3e8717952 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -96,9 +97,12 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan /// Phase 2: Scan all types and produce JavaPeerInfo records. /// public List Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + => Scan (assemblies.Select (a => new AssemblyInput (a.Name, "", a.Reader))); + + public List Scan (IEnumerable assemblies) { - foreach (var (name, reader) in assemblies) { - var index = AssemblyIndex.Create (reader, name); + foreach (var assembly in assemblies) { + var index = AssemblyIndex.Create (assembly.Reader, assembly.Name, assembly.Path); assemblyCache [index.AssemblyName] = index; } @@ -258,9 +262,23 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } } + if (!IsResolvableJavaPeerType (typeDef, index, out var unresolvedTypeName, out var unresolvedAssemblyName)) { + var unresolvedAssemblyPath = assemblyCache.TryGetValue (unresolvedAssemblyName, out var unresolvedAssemblyIndex) + ? unresolvedAssemblyIndex.AssemblyPath + : ""; + logger?.LogUnresolvableJavaPeerSkippedWarning (fullName, index.AssemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); + continue; + } + + var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; + if (isGenericDefinition && + frameworkAssemblyNames.Contains (index.AssemblyName) && + !IsSupportedFrameworkGenericPeer (fullName, index.AssemblyName)) { + continue; + } + var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; - var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; var isUnconditional = attrInfo is not null; var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo; @@ -325,6 +343,212 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } } + static bool IsSupportedFrameworkGenericPeer (string managedTypeName, string assemblyName) + { + if (!string.Equals (assemblyName, "Mono.Android", StringComparison.Ordinal)) { + return false; + } + + return managedTypeName is + "Android.Runtime.JavaCollection`1" or + "Android.Runtime.JavaDictionary`2" or + "Android.Runtime.JavaList`1" or + "Android.Runtime.JavaSet`1"; + } + + bool IsResolvableJavaPeerType ( + TypeDefinition typeDef, + AssemblyIndex index, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + var visited = new HashSet<(string AssemblyName, string TypeName)> (); + return IsResolvableTypeDefinition (typeDef, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + } + + bool IsResolvableTypeDefinition ( + TypeDefinition typeDef, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + var typeName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + if (!visited.Add ((index.AssemblyName, typeName))) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + if (!IsResolvableTypeHandle (typeDef.BaseType, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + return false; + } + + foreach (var interfaceHandle in typeDef.GetInterfaceImplementations ()) { + var interfaceImplementation = index.Reader.GetInterfaceImplementation (interfaceHandle); + if (!IsResolvableTypeHandle (interfaceImplementation.Interface, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + return false; + } + } + + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + bool IsResolvableTypeHandle ( + EntityHandle handle, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + if (handle.IsNil) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + switch (handle.Kind) { + case HandleKind.TypeDefinition: + return IsResolvableTypeDefinition (index.Reader.GetTypeDefinition ((TypeDefinitionHandle) handle), index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + case HandleKind.TypeReference: + return IsResolvableTypeReference ((TypeReferenceHandle) handle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + case HandleKind.TypeSpecification: + return IsResolvableTypeSpecification ((TypeSpecificationHandle) handle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + default: + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + } + + bool IsResolvableTypeReference ( + TypeReferenceHandle handle, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + var (typeName, assemblyName) = ResolveTypeReference (handle, index); + if (!assemblyCache.TryGetValue (assemblyName, out var resolvedIndex)) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + if (resolvedIndex.TypesByFullName.TryGetValue (typeName, out var typeHandle)) { + return IsResolvableTypeDefinition (resolvedIndex.Reader.GetTypeDefinition (typeHandle), resolvedIndex, visited, out unresolvedTypeName, out unresolvedAssemblyName); + } + + if (resolvedIndex.ExportedTypeNames.Contains (typeName)) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + unresolvedTypeName = typeName; + unresolvedAssemblyName = assemblyName; + return false; + } + + bool IsResolvableTypeSpecification ( + TypeSpecificationHandle handle, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + var reader = index.Reader.GetBlobReader (index.Reader.GetTypeSpecification (handle).Signature); + return IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + } + + bool IsResolvableSignatureType ( + ref BlobReader reader, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + if (reader.RemainingBytes == 0) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + + var typeCode = reader.ReadByte (); + switch (typeCode) { + case 0x11: + case 0x12: + return IsResolvableTypeDefOrRefEncodedHandle (reader.ReadCompressedInteger (), index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + case 0x13: + case 0x1e: + reader.ReadCompressedInteger (); + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + case 0x1d: + return IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + case 0x14: + if (!IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + return false; + } + SkipArrayShape (ref reader); + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + case 0x15: + reader.ReadByte (); + if (!IsResolvableTypeDefOrRefEncodedHandle (reader.ReadCompressedInteger (), index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + return false; + } + int genericArgumentCount = reader.ReadCompressedInteger (); + for (int i = 0; i < genericArgumentCount; i++) { + if (!IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + return false; + } + } + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + default: + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } + } + + bool IsResolvableTypeDefOrRefEncodedHandle ( + int encodedHandle, + AssemblyIndex index, + HashSet<(string AssemblyName, string TypeName)> visited, + [NotNullWhen (false)] out string? unresolvedTypeName, + [NotNullWhen (false)] out string? unresolvedAssemblyName) + { + int tag = encodedHandle & 0x3; + int row = encodedHandle >> 2; + EntityHandle handle = tag switch { + 0 => MetadataTokens.TypeDefinitionHandle (row), + 1 => MetadataTokens.TypeReferenceHandle (row), + 2 => MetadataTokens.TypeSpecificationHandle (row), + _ => default, + }; + return IsResolvableTypeHandle (handle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + } + + static void SkipArrayShape (ref BlobReader reader) + { + reader.ReadCompressedInteger (); + int sizes = reader.ReadCompressedInteger (); + for (int i = 0; i < sizes; i++) { + reader.ReadCompressedInteger (); + } + int lowerBounds = reader.ReadCompressedInteger (); + for (int i = 0; i < lowerBounds; i++) { + reader.ReadCompressedSignedInteger (); + } + } + (List, List) CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index, bool detectBaseOverrides) { var methods = new List (); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 95d0446b205..416c461fe28 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -27,7 +27,7 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) /// No file IO is performed — all results are returned in memory. /// public TrimmableTypeMapResult Execute ( - IReadOnlyList<(string Name, PEReader Reader)> assemblies, + IReadOnlyList assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, @@ -155,7 +155,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) { using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); var peers = scanner.Scan (assemblies); diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 42e59329019..1703ab161f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1605,6 +1605,15 @@ public static string XA4255 { } } + /// + /// Looks up a localized string similar to Skipping Java peer type '{0}' from assembly '{1}' because referenced type '{2}' from assembly '{3}' could not be resolved in '{4}'. This type will not be included in the trimmable type map.. + /// + public static string XA4256 { + get { + return ResourceManager.GetString("XA4256", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index f41614e9e2a..0d434afc855 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1165,6 +1165,15 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Generated trimmable type map Java source '{0}' was not found. The following are literal names and should not be translated: trimmable type map, Java. {0} - Full path to the generated Java source file + + + Skipping Java peer type '{0}' from assembly '{1}' because referenced type '{2}' from assembly '{3}' could not be resolved in '{4}'. This type will not be included in the trimmable type map. + The following are literal names and should not be translated: Java, trimmable type map. +{0} - Fully-qualified managed Java peer type name +{1} - Assembly containing the skipped Java peer type +{2} - Fully-qualified managed referenced type name that could not be resolved +{3} - Assembly expected to contain the unresolved type +{4} - Full path to the resolved assembly file that was expected to contain the unresolved type Command '{0}' failed.\n{1} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 9a03ef4ff21..749ff5953d6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -43,6 +43,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); + public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) => + log.LogCodedWarning ("XA4256", Properties.Resources.XA4256, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); } @@ -135,7 +137,7 @@ public override bool RunTask () Directory.CreateDirectory (JavaSourceOutputDirectory); var peReaders = new List (); - var assemblies = new List<(string Name, PEReader Reader)> (); + var assemblies = new List (); TrimmableTypeMapResult? result = null; try { foreach (var (path, isFrameworkAssembly) in assemblyInputs) { @@ -143,7 +145,7 @@ public override bool RunTask () peReaders.Add (peReader); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - assemblies.Add ((assemblyName, peReader)); + assemblies.Add (new AssemblyInput (assemblyName, path, peReader)); if (isFrameworkAssembly) { frameworkAssemblyNames.Add (assemblyName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index b3eba523be1..463277fdcb9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -34,6 +34,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); + public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) => + warnings?.Add ($"Skipping Java peer '{managedTypeName}' from '{assemblyName}' because referenced type '{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); } @@ -55,7 +57,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location; using var peReader = new PEReader (File.OpenRead (testAssemblyPath)); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("TestAssembly", peReader) }, + new [] { Input ("TestAssembly", peReader) }, new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); @@ -67,7 +69,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -78,7 +80,7 @@ public void Execute_WithTestFixtures_ProducesOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -139,7 +141,7 @@ public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendan [Fact] public void Execute_NullAssemblyList_Throws () { - IReadOnlyList<(string Name, PEReader Reader)>? n = null; + IReadOnlyList? n = null; #pragma warning disable CS8604 Assert.Throws (() => CreateGenerator ().Execute (n, new Version (11, 0), new HashSet ())); #pragma warning restore CS8604 @@ -149,7 +151,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -162,7 +164,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -172,7 +174,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("Mono.Android", peReader) }, + new [] { Input ("Mono.Android", peReader) }, new Version (11, 0), new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }); @@ -196,7 +198,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () """); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet (), useSharedTypemapUniverse: false, @@ -217,6 +219,8 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); + [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] From f94e32672e1df5b27626da8deece1add8e94c6b9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:13:56 +0200 Subject: [PATCH 02/11] Add XA4256 scanner regression test Cover Java peers skipped because their base type or implemented interface references a type missing from a resolved assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGeneratorTests.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 463277fdcb9..bede22eb8d2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using Xunit; @@ -214,6 +216,33 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); } + [Theory] + [InlineData (false, "MissingDependency.MissingBase")] + [InlineData (true, "MissingDependency.IMissingInterface")] + public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool useMissingInterface, string unresolvedTypeName) + { + var warnings = new List (); + using var peerStream = CreateStaleJavaPeerAssembly (useMissingInterface); + using var missingDependencyStream = CreateEmptyAssembly ("MissingDependency"); + using var peerReader = new PEReader (peerStream, PEStreamOptions.LeaveOpen); + using var missingDependencyReader = new PEReader (missingDependencyStream, PEStreamOptions.LeaveOpen); + + var result = CreateGenerator (warnings).Execute ( + new [] { + new AssemblyInput ("StalePeerAssembly", "/tmp/StalePeerAssembly.dll", peerReader), + new AssemblyInput ("MissingDependency", "/tmp/MissingDependency.dll", missingDependencyReader), + }, + new Version (11, 0), + new HashSet ()); + + Assert.DoesNotContain (result.AllPeers, p => p.ManagedTypeName == "Test.BrokenPeer"); + var warning = Assert.Single (warnings); + Assert.Contains ("Test.BrokenPeer", warning); + Assert.Contains (unresolvedTypeName, warning); + Assert.Contains ("MissingDependency", warning); + Assert.Contains ("/tmp/MissingDependency.dll", warning); + } + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); TrimmableTypeMapGenerator CreateGenerator (List warnings) => @@ -221,6 +250,62 @@ TrimmableTypeMapGenerator CreateGenerator (List warnings) => static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); + static MemoryStream CreateEmptyAssembly (string assemblyName) + { + var stream = new MemoryStream (); + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble (assemblyName, assemblyName + ".dll"); + pe.WritePE (stream); + stream.Position = 0; + return stream; + } + + static MemoryStream CreateStaleJavaPeerAssembly (bool useMissingInterface) + { + var stream = new MemoryStream (); + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("StalePeerAssembly", "StalePeerAssembly.dll"); + + var missingDependencyRef = pe.FindOrAddAssemblyRef ("MissingDependency"); + var missingTypeRef = pe.Metadata.AddTypeReference ( + missingDependencyRef, + pe.Metadata.GetOrAddString ("MissingDependency"), + pe.Metadata.GetOrAddString (useMissingInterface ? "IMissingInterface" : "MissingBase")); + var objectRef = pe.Metadata.AddTypeReference ( + pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), + pe.Metadata.GetOrAddString ("Object")); + + var registerAttributeRef = pe.Metadata.AddTypeReference ( + pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Android.Runtime"), + pe.Metadata.GetOrAddString ("RegisterAttribute")); + var registerCtorRef = pe.AddMemberRef (registerAttributeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); + + var peerType = pe.Metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("BrokenPeer"), + useMissingInterface ? objectRef : missingTypeRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + pe.Metadata.AddCustomAttribute ( + peerType, + registerCtorRef, + pe.BuildAttributeBlob (b => b.WriteSerializedString ("test/BrokenPeer"))); + + if (useMissingInterface) { + pe.Metadata.AddInterfaceImplementation (peerType, missingTypeRef); + } + + pe.WritePE (stream); + stream.Position = 0; + return stream; + } + [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] From 3e3698605e659b630fec850ce7b597164db3be42 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:16:41 +0200 Subject: [PATCH 03/11] Use platform-neutral XA4256 test paths Keep the synthetic assembly paths in the XA4256 regression test platform-neutral. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index bede22eb8d2..4051b1c7d1f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -222,6 +222,8 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool useMissingInterface, string unresolvedTypeName) { var warnings = new List (); + var peerPath = Path.Combine (Path.GetTempPath (), "StalePeerAssembly.dll"); + var missingDependencyPath = Path.Combine (Path.GetTempPath (), "MissingDependency.dll"); using var peerStream = CreateStaleJavaPeerAssembly (useMissingInterface); using var missingDependencyStream = CreateEmptyAssembly ("MissingDependency"); using var peerReader = new PEReader (peerStream, PEStreamOptions.LeaveOpen); @@ -229,8 +231,8 @@ public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool us var result = CreateGenerator (warnings).Execute ( new [] { - new AssemblyInput ("StalePeerAssembly", "/tmp/StalePeerAssembly.dll", peerReader), - new AssemblyInput ("MissingDependency", "/tmp/MissingDependency.dll", missingDependencyReader), + new AssemblyInput ("StalePeerAssembly", peerPath, peerReader), + new AssemblyInput ("MissingDependency", missingDependencyPath, missingDependencyReader), }, new Version (11, 0), new HashSet ()); @@ -240,7 +242,7 @@ public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool us Assert.Contains ("Test.BrokenPeer", warning); Assert.Contains (unresolvedTypeName, warning); Assert.Contains ("MissingDependency", warning); - Assert.Contains ("/tmp/MissingDependency.dll", warning); + Assert.Contains (missingDependencyPath, warning); } TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); From 9d8935036d642e1c8fe6633817cb0395aba3b693 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:19:22 +0200 Subject: [PATCH 04/11] Use collection expressions in XA4256 tests Replace legacy new-array expressions in the updated trimmable typemap generator tests with C# collection expressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGeneratorTests.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 4051b1c7d1f..d45f0263c4b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -59,7 +59,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location; using var peReader = new PEReader (File.OpenRead (testAssemblyPath)); var result = CreateGenerator ().Execute ( - new [] { Input ("TestAssembly", peReader) }, + [Input ("TestAssembly", peReader)], new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); @@ -71,7 +71,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -82,7 +82,7 @@ public void Execute_WithTestFixtures_ProducesOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -153,7 +153,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -166,7 +166,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new [] { Input ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -176,7 +176,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ( - new [] { Input ("Mono.Android", peReader) }, + [Input ("Mono.Android", peReader)], new Version (11, 0), new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }); @@ -200,7 +200,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () """); var result = CreateGenerator ().Execute ( - new [] { Input ("TestFixtures", peReader) }, + [Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet (), useSharedTypemapUniverse: false, @@ -230,10 +230,10 @@ public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool us using var missingDependencyReader = new PEReader (missingDependencyStream, PEStreamOptions.LeaveOpen); var result = CreateGenerator (warnings).Execute ( - new [] { + [ new AssemblyInput ("StalePeerAssembly", peerPath, peerReader), new AssemblyInput ("MissingDependency", missingDependencyPath, missingDependencyReader), - }, + ], new Version (11, 0), new HashSet ()); From e7eaed4abb936cad70ed182a9c0e776b692c2cae Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 14:00:29 +0200 Subject: [PATCH 05/11] Address XA4256 scanner review feedback Remove the framework generic peer allow-list, memoize resolvability checks, use named signature metadata enums, and extend the regression test to cover generic-inst TypeSpec references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 7 +- .../Scanner/JavaPeerScanner.cs | 69 ++++++++++--------- .../TrimmableTypeMapGenerator.cs | 5 +- .../Tasks/GenerateTrimmableTypeMap.cs | 7 +- .../TrimmableTypeMapGeneratorTests.cs | 69 ++++++++++++++++--- 5 files changed, 110 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 87d6c70cd46..cdc5cf040fe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -12,6 +12,11 @@ public interface ITrimmableTypeMapLogger void LogGeneratedJcwFilesInfo (int sourceCount); void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); - void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath); + void LogUnresolvableJavaPeerSkippedWarning ( + string managedTypeName, + string assemblyName, + string unresolvedTypeName, + string unresolvedAssemblyName, + string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 9c3e8717952..01765df2e07 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -23,8 +23,11 @@ enum HashedPackageNamingPolicy { LowercaseCrc64, } + readonly record struct ResolvabilityResult (bool IsResolvable, string? UnresolvedTypeName, string? UnresolvedAssemblyName); + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + readonly Dictionary<(string AssemblyName, string TypeName), ResolvabilityResult> resolvabilityCache = new (); readonly ITrimmableTypeMapLogger? logger; readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; @@ -271,12 +274,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; - if (isGenericDefinition && - frameworkAssemblyNames.Contains (index.AssemblyName) && - !IsSupportedFrameworkGenericPeer (fullName, index.AssemblyName)) { - continue; - } - var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; @@ -343,19 +340,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } } - static bool IsSupportedFrameworkGenericPeer (string managedTypeName, string assemblyName) - { - if (!string.Equals (assemblyName, "Mono.Android", StringComparison.Ordinal)) { - return false; - } - - return managedTypeName is - "Android.Runtime.JavaCollection`1" or - "Android.Runtime.JavaDictionary`2" or - "Android.Runtime.JavaList`1" or - "Android.Runtime.JavaSet`1"; - } - bool IsResolvableJavaPeerType ( TypeDefinition typeDef, AssemblyIndex index, @@ -374,25 +358,36 @@ bool IsResolvableTypeDefinition ( [NotNullWhen (false)] out string? unresolvedAssemblyName) { var typeName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - if (!visited.Add ((index.AssemblyName, typeName))) { + var cacheKey = (index.AssemblyName, typeName); + + if (resolvabilityCache.TryGetValue (cacheKey, out var cached)) { + unresolvedTypeName = cached.UnresolvedTypeName; + unresolvedAssemblyName = cached.UnresolvedAssemblyName; + return cached.IsResolvable; + } + + if (!visited.Add (cacheKey)) { unresolvedTypeName = null; unresolvedAssemblyName = null; return true; } if (!IsResolvableTypeHandle (typeDef.BaseType, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; } foreach (var interfaceHandle in typeDef.GetInterfaceImplementations ()) { var interfaceImplementation = index.Reader.GetInterfaceImplementation (interfaceHandle); if (!IsResolvableTypeHandle (interfaceImplementation.Interface, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; } } unresolvedTypeName = null; unresolvedAssemblyName = null; + resolvabilityCache [cacheKey] = new (true, null, null); return true; } @@ -476,20 +471,22 @@ bool IsResolvableSignatureType ( return true; } - var typeCode = reader.ReadByte (); - switch (typeCode) { - case 0x11: - case 0x12: + var rawTypeCode = reader.ReadByte (); + if ((SignatureTypeKind) rawTypeCode is SignatureTypeKind.ValueType or SignatureTypeKind.Class) { return IsResolvableTypeDefOrRefEncodedHandle (reader.ReadCompressedInteger (), index, visited, out unresolvedTypeName, out unresolvedAssemblyName); - case 0x13: - case 0x1e: + } + + var typeCode = (SignatureTypeCode) rawTypeCode; + switch (typeCode) { + case SignatureTypeCode.GenericTypeParameter: + case SignatureTypeCode.GenericMethodParameter: reader.ReadCompressedInteger (); unresolvedTypeName = null; unresolvedAssemblyName = null; return true; - case 0x1d: + case SignatureTypeCode.SZArray: return IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); - case 0x14: + case SignatureTypeCode.Array: if (!IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { return false; } @@ -497,8 +494,12 @@ bool IsResolvableSignatureType ( unresolvedTypeName = null; unresolvedAssemblyName = null; return true; - case 0x15: - reader.ReadByte (); + case SignatureTypeCode.GenericTypeInstance: + if ((SignatureTypeKind) reader.ReadByte () is not (SignatureTypeKind.ValueType or SignatureTypeKind.Class)) { + unresolvedTypeName = null; + unresolvedAssemblyName = null; + return true; + } if (!IsResolvableTypeDefOrRefEncodedHandle (reader.ReadCompressedInteger (), index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { return false; } @@ -1860,13 +1861,13 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI 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 + var elementType = (SignatureTypeCode) blobReader.ReadByte (); + if (elementType != SignatureTypeCode.GenericTypeInstance) { return null; } - var classOrValueType = blobReader.ReadByte (); - if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE + var classOrValueType = (SignatureTypeKind) blobReader.ReadByte (); + if (classOrValueType is not (SignatureTypeKind.Class or SignatureTypeKind.ValueType)) { return null; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 416c461fe28..4d2d75a4620 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -155,7 +155,10 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies ( + IReadOnlyList assemblies, + string? packageNamingPolicy, + HashSet frameworkAssemblyNames) { using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); var peers = scanner.Scan (assemblies); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 749ff5953d6..8699b371bcd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -43,7 +43,12 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); - public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) => + public void LogUnresolvableJavaPeerSkippedWarning ( + string managedTypeName, + string assemblyName, + string unresolvedTypeName, + string unresolvedAssemblyName, + string unresolvedAssemblyPath) => log.LogCodedWarning ("XA4256", Properties.Resources.XA4256, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index d45f0263c4b..dfe0ee4c6a4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -36,8 +36,15 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); - public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) => - warnings?.Add ($"Skipping Java peer '{managedTypeName}' from '{assemblyName}' because referenced type '{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); + public void LogUnresolvableJavaPeerSkippedWarning ( + string managedTypeName, + string assemblyName, + string unresolvedTypeName, + string unresolvedAssemblyName, + string unresolvedAssemblyPath) => + warnings?.Add ( + $"Skipping Java peer '{managedTypeName}' from '{assemblyName}' because referenced type " + + $"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); } @@ -217,14 +224,15 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () } [Theory] - [InlineData (false, "MissingDependency.MissingBase")] - [InlineData (true, "MissingDependency.IMissingInterface")] - public void Execute_SkipsJavaPeerWithUnresolvableBaseOrInterfaceTypeRef (bool useMissingInterface, string unresolvedTypeName) + [InlineData (StaleReferenceShape.BaseType, "MissingDependency.MissingBase")] + [InlineData (StaleReferenceShape.Interface, "MissingDependency.IMissingInterface")] + [InlineData (StaleReferenceShape.GenericBaseArgument, "MissingDependency.MissingArgument")] + public void Execute_SkipsJavaPeerWithUnresolvableBaseInterfaceOrGenericArgumentTypeRef (StaleReferenceShape shape, string unresolvedTypeName) { var warnings = new List (); var peerPath = Path.Combine (Path.GetTempPath (), "StalePeerAssembly.dll"); var missingDependencyPath = Path.Combine (Path.GetTempPath (), "MissingDependency.dll"); - using var peerStream = CreateStaleJavaPeerAssembly (useMissingInterface); + using var peerStream = CreateStaleJavaPeerAssembly (shape); using var missingDependencyStream = CreateEmptyAssembly ("MissingDependency"); using var peerReader = new PEReader (peerStream, PEStreamOptions.LeaveOpen); using var missingDependencyReader = new PEReader (missingDependencyStream, PEStreamOptions.LeaveOpen); @@ -252,6 +260,12 @@ TrimmableTypeMapGenerator CreateGenerator (List warnings) => static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); + enum StaleReferenceShape { + BaseType, + Interface, + GenericBaseArgument, + } + static MemoryStream CreateEmptyAssembly (string assemblyName) { var stream = new MemoryStream (); @@ -262,21 +276,31 @@ static MemoryStream CreateEmptyAssembly (string assemblyName) return stream; } - static MemoryStream CreateStaleJavaPeerAssembly (bool useMissingInterface) + static MemoryStream CreateStaleJavaPeerAssembly (StaleReferenceShape shape) { var stream = new MemoryStream (); var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); pe.EmitPreamble ("StalePeerAssembly", "StalePeerAssembly.dll"); + var missingTypeName = shape switch { + StaleReferenceShape.Interface => "IMissingInterface", + StaleReferenceShape.GenericBaseArgument => "MissingArgument", + _ => "MissingBase", + }; var missingDependencyRef = pe.FindOrAddAssemblyRef ("MissingDependency"); var missingTypeRef = pe.Metadata.AddTypeReference ( missingDependencyRef, pe.Metadata.GetOrAddString ("MissingDependency"), - pe.Metadata.GetOrAddString (useMissingInterface ? "IMissingInterface" : "MissingBase")); + pe.Metadata.GetOrAddString (missingTypeName)); var objectRef = pe.Metadata.AddTypeReference ( pe.SystemRuntimeRef, pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + var peerBaseType = shape switch { + StaleReferenceShape.BaseType => missingTypeRef, + StaleReferenceShape.GenericBaseArgument => CreateGenericBaseTypeSpec (pe, objectRef, missingTypeRef), + _ => objectRef, + }; var registerAttributeRef = pe.Metadata.AddTypeReference ( pe.MonoAndroidRef, @@ -291,7 +315,7 @@ static MemoryStream CreateStaleJavaPeerAssembly (bool useMissingInterface) TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, pe.Metadata.GetOrAddString ("Test"), pe.Metadata.GetOrAddString ("BrokenPeer"), - useMissingInterface ? objectRef : missingTypeRef, + peerBaseType, MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); pe.Metadata.AddCustomAttribute ( @@ -299,7 +323,7 @@ static MemoryStream CreateStaleJavaPeerAssembly (bool useMissingInterface) registerCtorRef, pe.BuildAttributeBlob (b => b.WriteSerializedString ("test/BrokenPeer"))); - if (useMissingInterface) { + if (shape == StaleReferenceShape.Interface) { pe.Metadata.AddInterfaceImplementation (peerType, missingTypeRef); } @@ -308,6 +332,31 @@ static MemoryStream CreateStaleJavaPeerAssembly (bool useMissingInterface) return stream; } + static TypeSpecificationHandle CreateGenericBaseTypeSpec (PEAssemblyBuilder pe, EntityHandle objectRef, TypeReferenceHandle missingTypeRef) + { + var genericBaseType = pe.Metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("GenericBase`1"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + pe.Metadata.AddGenericParameter ( + genericBaseType, + GenericParameterAttributes.None, + pe.Metadata.GetOrAddString ("T"), + 0); + + var signature = new BlobBuilder (); + signature.WriteByte ((byte) SignatureTypeCode.GenericTypeInstance); + signature.WriteByte ((byte) SignatureTypeKind.Class); + signature.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (genericBaseType)); + signature.WriteCompressedInteger (1); + signature.WriteByte ((byte) SignatureTypeKind.Class); + signature.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (missingTypeRef)); + return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (signature)); + } + [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] From b1dd9b28931400ffce9ce7640208fa1b0f4e2b81 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 14:27:47 +0200 Subject: [PATCH 06/11] Fix XA4256 test data accessibility Make the nested StaleReferenceShape enum public so the public xUnit theory method has a consistent public signature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index dfe0ee4c6a4..f7eaba5de4d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -260,7 +260,7 @@ TrimmableTypeMapGenerator CreateGenerator (List warnings) => static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); - enum StaleReferenceShape { + public enum StaleReferenceShape { BaseType, Interface, GenericBaseArgument, From 6d6786bf1e73a1b324128b27b8e54bb656e341c7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 15:11:09 +0200 Subject: [PATCH 07/11] Fix CS8506 build break in XA4256 test The peerBaseType switch expression mixed TypeReferenceHandle and TypeSpecificationHandle arms, which have no common type (CS8506). Annotate the target as EntityHandle, which both handle types convert to and which is what AddTypeDefinition's baseType parameter expects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index f7eaba5de4d..d9ab0a0bdfd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -296,7 +296,7 @@ static MemoryStream CreateStaleJavaPeerAssembly (StaleReferenceShape shape) pe.SystemRuntimeRef, pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); - var peerBaseType = shape switch { + EntityHandle peerBaseType = shape switch { StaleReferenceShape.BaseType => missingTypeRef, StaleReferenceShape.GenericBaseArgument => CreateGenericBaseTypeSpec (pe, objectRef, missingTypeRef), _ => objectRef, From 68e675e08757564e5e83fc2a3125c9c0584b3be5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 15:42:02 +0200 Subject: [PATCH 08/11] Key trimmable typemap resolvability cache by type handle The per-scan resolvability cache and the per-peer visited set were keyed by (assembly name, full type name), which rebuilt the full type name string on every type-definition visit including cache hits. Shared ancestors across AndroidX/Material are revalidated for many sibling peers, so this allocated repeatedly on a hot path. Key both by (assembly name, type-definition row) instead. The row is a cheap, stable identifier within an assembly, so no full name is built unless XA4256 is actually logged. Using the handle also removes the generic-name-collapsing ambiguity (e.g. Base vs Base) that a name-based key could introduce in the cycle guard. Also document why the signature parser defaults unrecognized encodings to resolvable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 01765df2e07..66637443585 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -27,7 +27,7 @@ enum HashedPackageNamingPolicy { readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); - readonly Dictionary<(string AssemblyName, string TypeName), ResolvabilityResult> resolvabilityCache = new (); + readonly Dictionary<(string AssemblyName, int TypeRow), ResolvabilityResult> resolvabilityCache = new (); readonly ITrimmableTypeMapLogger? logger; readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; @@ -265,7 +265,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } } - if (!IsResolvableJavaPeerType (typeDef, index, out var unresolvedTypeName, out var unresolvedAssemblyName)) { + if (!IsResolvableJavaPeerType (typeHandle, index, out var unresolvedTypeName, out var unresolvedAssemblyName)) { var unresolvedAssemblyPath = assemblyCache.TryGetValue (unresolvedAssemblyName, out var unresolvedAssemblyIndex) ? unresolvedAssemblyIndex.AssemblyPath : ""; @@ -341,24 +341,27 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } bool IsResolvableJavaPeerType ( - TypeDefinition typeDef, + TypeDefinitionHandle typeDefHandle, AssemblyIndex index, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { - var visited = new HashSet<(string AssemblyName, string TypeName)> (); - return IsResolvableTypeDefinition (typeDef, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + // The base/interface graph of valid managed metadata is acyclic, so the + // per-call visited set only guards against generic self-references (e.g. + // class Foo : Bar). Keying it (and the cache) by type-definition row + // avoids building full type names on the hot path and on cache hits. + var visited = new HashSet<(string AssemblyName, int TypeRow)> (); + return IsResolvableTypeDefinition (typeDefHandle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); } bool IsResolvableTypeDefinition ( - TypeDefinition typeDef, + TypeDefinitionHandle typeDefHandle, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { - var typeName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var cacheKey = (index.AssemblyName, typeName); + var cacheKey = (index.AssemblyName, MetadataTokens.GetRowNumber (typeDefHandle)); if (resolvabilityCache.TryGetValue (cacheKey, out var cached)) { unresolvedTypeName = cached.UnresolvedTypeName; @@ -372,6 +375,8 @@ bool IsResolvableTypeDefinition ( return true; } + var typeDef = index.Reader.GetTypeDefinition (typeDefHandle); + if (!IsResolvableTypeHandle (typeDef.BaseType, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; @@ -394,7 +399,7 @@ bool IsResolvableTypeDefinition ( bool IsResolvableTypeHandle ( EntityHandle handle, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { @@ -406,7 +411,7 @@ bool IsResolvableTypeHandle ( switch (handle.Kind) { case HandleKind.TypeDefinition: - return IsResolvableTypeDefinition (index.Reader.GetTypeDefinition ((TypeDefinitionHandle) handle), index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + return IsResolvableTypeDefinition ((TypeDefinitionHandle) handle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); case HandleKind.TypeReference: return IsResolvableTypeReference ((TypeReferenceHandle) handle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); case HandleKind.TypeSpecification: @@ -421,7 +426,7 @@ bool IsResolvableTypeHandle ( bool IsResolvableTypeReference ( TypeReferenceHandle handle, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { @@ -433,7 +438,7 @@ bool IsResolvableTypeReference ( } if (resolvedIndex.TypesByFullName.TryGetValue (typeName, out var typeHandle)) { - return IsResolvableTypeDefinition (resolvedIndex.Reader.GetTypeDefinition (typeHandle), resolvedIndex, visited, out unresolvedTypeName, out unresolvedAssemblyName); + return IsResolvableTypeDefinition (typeHandle, resolvedIndex, visited, out unresolvedTypeName, out unresolvedAssemblyName); } if (resolvedIndex.ExportedTypeNames.Contains (typeName)) { @@ -450,7 +455,7 @@ bool IsResolvableTypeReference ( bool IsResolvableTypeSpecification ( TypeSpecificationHandle handle, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { @@ -461,7 +466,7 @@ bool IsResolvableTypeSpecification ( bool IsResolvableSignatureType ( ref BlobReader reader, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { @@ -513,6 +518,9 @@ bool IsResolvableSignatureType ( unresolvedAssemblyName = null; return true; default: + // Pointers, byrefs, custom modifiers, and any encoding we don't + // specifically decode default to resolvable so we never wrongly skip + // a Java peer over metadata shapes this scanner doesn't model. unresolvedTypeName = null; unresolvedAssemblyName = null; return true; @@ -522,7 +530,7 @@ bool IsResolvableSignatureType ( bool IsResolvableTypeDefOrRefEncodedHandle ( int encodedHandle, AssemblyIndex index, - HashSet<(string AssemblyName, string TypeName)> visited, + HashSet<(string AssemblyName, int TypeRow)> visited, [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { From fe5df6457f050171b062aed4c699693237fcd071 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 15:42:02 +0200 Subject: [PATCH 09/11] Add XA4256 negative tests: unscanned ref and type-forwarding Cover the two conservative branches of the resolvability walk that keep a Java peer instead of skipping it: - The referenced assembly is not part of the scanned set, so the reference cannot be proven stale (existing behavior preserved). - The referenced type is re-exported via a type-forwarder row and is resolved through AssemblyIndex.ExportedTypeNames. Both assert the peer is kept and no XA4256 is emitted, guarding the resolvability walk (and the new handle-keyed cache) against regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGeneratorTests.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index d9ab0a0bdfd..932f7bd2ff7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -253,6 +253,52 @@ public void Execute_SkipsJavaPeerWithUnresolvableBaseInterfaceOrGenericArgumentT Assert.Contains (missingDependencyPath, warning); } + [Fact] + public void Execute_DoesNotSkipJavaPeer_WhenReferencedAssemblyIsNotScanned () + { + // The base type's assembly ('MissingDependency') is not part of the scanned + // set, so the scanner cannot prove the reference is stale. Existing behavior + // must be preserved: the peer is kept and no XA4256 is emitted. + var warnings = new List (); + var peerPath = Path.Combine (Path.GetTempPath (), "StalePeerAssembly.dll"); + using var peerStream = CreateStaleJavaPeerAssembly (StaleReferenceShape.BaseType); + using var peerReader = new PEReader (peerStream, PEStreamOptions.LeaveOpen); + + var result = CreateGenerator (warnings).Execute ( + [new AssemblyInput ("StalePeerAssembly", peerPath, peerReader)], + new Version (11, 0), + new HashSet ()); + + Assert.Contains (result.AllPeers, p => p.ManagedTypeName == "Test.BrokenPeer"); + Assert.Empty (warnings); + } + + [Fact] + public void Execute_DoesNotSkipJavaPeer_WhenBaseTypeIsTypeForwarded () + { + // 'MissingDependency' is scanned but does not define 'MissingBase' — it only + // re-exports it via a type-forward row. The scanner must treat the reference + // as resolvable through AssemblyIndex.ExportedTypeNames and keep the peer. + var warnings = new List (); + var peerPath = Path.Combine (Path.GetTempPath (), "StalePeerAssembly.dll"); + var forwardingPath = Path.Combine (Path.GetTempPath (), "MissingDependency.dll"); + using var peerStream = CreateStaleJavaPeerAssembly (StaleReferenceShape.BaseType); + using var forwardingStream = CreateAssemblyForwardingType ("MissingDependency", "MissingDependency", "MissingBase"); + using var peerReader = new PEReader (peerStream, PEStreamOptions.LeaveOpen); + using var forwardingReader = new PEReader (forwardingStream, PEStreamOptions.LeaveOpen); + + var result = CreateGenerator (warnings).Execute ( + [ + new AssemblyInput ("StalePeerAssembly", peerPath, peerReader), + new AssemblyInput ("MissingDependency", forwardingPath, forwardingReader), + ], + new Version (11, 0), + new HashSet ()); + + Assert.Contains (result.AllPeers, p => p.ManagedTypeName == "Test.BrokenPeer"); + Assert.Empty (warnings); + } + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); TrimmableTypeMapGenerator CreateGenerator (List warnings) => @@ -276,6 +322,26 @@ static MemoryStream CreateEmptyAssembly (string assemblyName) return stream; } + static MemoryStream CreateAssemblyForwardingType (string assemblyName, string ns, string typeName) + { + var stream = new MemoryStream (); + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble (assemblyName, assemblyName + ".dll"); + + var forwardTargetRef = pe.FindOrAddAssemblyRef ("ForwardTarget"); + // 0x00200000 is the type-forwarder flag (TypeAttributes has no named constant for it). + pe.Metadata.AddExportedType ( + (TypeAttributes) 0x00200000, + pe.Metadata.GetOrAddString (ns), + pe.Metadata.GetOrAddString (typeName), + forwardTargetRef, + typeDefinitionId: 0); + + pe.WritePE (stream); + stream.Position = 0; + return stream; + } + static MemoryStream CreateStaleJavaPeerAssembly (StaleReferenceShape shape) { var stream = new MemoryStream (); From 6b6a8385dbd75b9b55c7b6bfa2ed49bd0e8980fd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 17:22:30 +0200 Subject: [PATCH 10/11] Walk generic constraints in resolvability check; reuse visited set Address two optional review suggestions: - Generic-definition peers are rooted in the type map (the emitter ldtokens the open-generic target), so a generic-parameter constraint that references a type missing from the resolved assembly set would also fail to resolve at NativeAOT time. Walk constraints alongside the base type and implemented interfaces, and skip the peer with XA4256. Adds a GenericConstraint shape to the skip theory. - Reuse a single 'visited' HashSet across peers (cleared per call) instead of allocating one per candidate peer. The per-scan resolvability cache already memoizes results, so the set only needs to break per-walk cycles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 25 ++++++++++++++++--- .../TrimmableTypeMapGeneratorTests.cs | 12 +++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 66637443585..3fc20117f34 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -28,6 +28,7 @@ enum HashedPackageNamingPolicy { readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); readonly Dictionary<(string AssemblyName, int TypeRow), ResolvabilityResult> resolvabilityCache = new (); + readonly HashSet<(string AssemblyName, int TypeRow)> resolvabilityVisited = new (); readonly ITrimmableTypeMapLogger? logger; readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; @@ -348,10 +349,12 @@ bool IsResolvableJavaPeerType ( { // The base/interface graph of valid managed metadata is acyclic, so the // per-call visited set only guards against generic self-references (e.g. - // class Foo : Bar). Keying it (and the cache) by type-definition row - // avoids building full type names on the hot path and on cache hits. - var visited = new HashSet<(string AssemblyName, int TypeRow)> (); - return IsResolvableTypeDefinition (typeDefHandle, index, visited, out unresolvedTypeName, out unresolvedAssemblyName); + // class Foo : Bar). It is reused across peers and cleared per call to + // avoid an allocation for every candidate on large peer graphs. Keying it + // (and the cache) by type-definition row avoids building full type names on + // the hot path and on cache hits. + resolvabilityVisited.Clear (); + return IsResolvableTypeDefinition (typeDefHandle, index, resolvabilityVisited, out unresolvedTypeName, out unresolvedAssemblyName); } bool IsResolvableTypeDefinition ( @@ -390,6 +393,20 @@ bool IsResolvableTypeDefinition ( } } + // Generic-definition peers are rooted in the type map (the emitter ldtokens + // the open-generic target), so a constraint referencing a stale type would + // also fail to resolve at NativeAOT time. Walk constraints like base types. + foreach (var genericParameterHandle in typeDef.GetGenericParameters ()) { + var genericParameter = index.Reader.GetGenericParameter (genericParameterHandle); + foreach (var constraintHandle in genericParameter.GetConstraints ()) { + var constraint = index.Reader.GetGenericParameterConstraint (constraintHandle); + if (!IsResolvableTypeHandle (constraint.Type, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { + resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); + return false; + } + } + } + unresolvedTypeName = null; unresolvedAssemblyName = null; resolvabilityCache [cacheKey] = new (true, null, null); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 932f7bd2ff7..87af547d8b1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -227,6 +227,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () [InlineData (StaleReferenceShape.BaseType, "MissingDependency.MissingBase")] [InlineData (StaleReferenceShape.Interface, "MissingDependency.IMissingInterface")] [InlineData (StaleReferenceShape.GenericBaseArgument, "MissingDependency.MissingArgument")] + [InlineData (StaleReferenceShape.GenericConstraint, "MissingDependency.MissingConstraint")] public void Execute_SkipsJavaPeerWithUnresolvableBaseInterfaceOrGenericArgumentTypeRef (StaleReferenceShape shape, string unresolvedTypeName) { var warnings = new List (); @@ -310,6 +311,7 @@ public enum StaleReferenceShape { BaseType, Interface, GenericBaseArgument, + GenericConstraint, } static MemoryStream CreateEmptyAssembly (string assemblyName) @@ -351,6 +353,7 @@ static MemoryStream CreateStaleJavaPeerAssembly (StaleReferenceShape shape) var missingTypeName = shape switch { StaleReferenceShape.Interface => "IMissingInterface", StaleReferenceShape.GenericBaseArgument => "MissingArgument", + StaleReferenceShape.GenericConstraint => "MissingConstraint", _ => "MissingBase", }; var missingDependencyRef = pe.FindOrAddAssemblyRef ("MissingDependency"); @@ -393,6 +396,15 @@ static MemoryStream CreateStaleJavaPeerAssembly (StaleReferenceShape shape) pe.Metadata.AddInterfaceImplementation (peerType, missingTypeRef); } + if (shape == StaleReferenceShape.GenericConstraint) { + var genericParameter = pe.Metadata.AddGenericParameter ( + peerType, + GenericParameterAttributes.None, + pe.Metadata.GetOrAddString ("T"), + 0); + pe.Metadata.AddGenericParameterConstraint (genericParameter, missingTypeRef); + } + pe.WritePE (stream); stream.Position = 0; return stream; From 8ff2ca5de8bb8a9f02ffa33a4f1531254a6e0ffa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 17:10:04 +0200 Subject: [PATCH 11/11] Renumber XA4256 -> XA4257 after merge (main took XA4256) main's PR introduced XA4256 ('Only one ... allowed') so the unresolvable Java peer warning is renumbered to XA4257. This completes the merge conflict resolution for GenerateTrimmableTypeMap.cs, which auto-merged and kept the stale XA4256 reference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 4a275af148d..2ca8f412a48 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -50,7 +50,7 @@ public void LogUnresolvableJavaPeerSkippedWarning ( string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) => - log.LogCodedWarning ("XA4256", Properties.Resources.XA4256, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); + log.LogCodedWarning ("XA4257", Properties.Resources.XA4257, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); }