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..cdc5cf040fe 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs
@@ -12,5 +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 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..3fc20117f34 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;
@@ -22,8 +23,12 @@ 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, int TypeRow), ResolvabilityResult> resolvabilityCache = new ();
+ readonly HashSet<(string AssemblyName, int TypeRow)> resolvabilityVisited = new ();
readonly ITrimmableTypeMapLogger? logger;
readonly HashedPackageNamingPolicy packageNamingPolicy;
readonly HashSet frameworkAssemblyNames;
@@ -96,9 +101,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 +266,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
}
}
+ if (!IsResolvableJavaPeerType (typeHandle, 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;
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 +341,240 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
}
}
+ bool IsResolvableJavaPeerType (
+ TypeDefinitionHandle typeDefHandle,
+ AssemblyIndex index,
+ [NotNullWhen (false)] out string? unresolvedTypeName,
+ [NotNullWhen (false)] out string? 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). 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 (
+ TypeDefinitionHandle typeDefHandle,
+ AssemblyIndex index,
+ HashSet<(string AssemblyName, int TypeRow)> visited,
+ [NotNullWhen (false)] out string? unresolvedTypeName,
+ [NotNullWhen (false)] out string? unresolvedAssemblyName)
+ {
+ var cacheKey = (index.AssemblyName, MetadataTokens.GetRowNumber (typeDefHandle));
+
+ 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;
+ }
+
+ var typeDef = index.Reader.GetTypeDefinition (typeDefHandle);
+
+ 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;
+ }
+ }
+
+ // 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);
+ return true;
+ }
+
+ bool IsResolvableTypeHandle (
+ EntityHandle handle,
+ AssemblyIndex index,
+ HashSet<(string AssemblyName, int TypeRow)> 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 ((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, int TypeRow)> 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 (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, int TypeRow)> 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, int TypeRow)> visited,
+ [NotNullWhen (false)] out string? unresolvedTypeName,
+ [NotNullWhen (false)] out string? unresolvedAssemblyName)
+ {
+ if (reader.RemainingBytes == 0) {
+ unresolvedTypeName = null;
+ unresolvedAssemblyName = null;
+ return true;
+ }
+
+ var rawTypeCode = reader.ReadByte ();
+ if ((SignatureTypeKind) rawTypeCode is SignatureTypeKind.ValueType or SignatureTypeKind.Class) {
+ return IsResolvableTypeDefOrRefEncodedHandle (reader.ReadCompressedInteger (), index, visited, out unresolvedTypeName, out unresolvedAssemblyName);
+ }
+
+ var typeCode = (SignatureTypeCode) rawTypeCode;
+ switch (typeCode) {
+ case SignatureTypeCode.GenericTypeParameter:
+ case SignatureTypeCode.GenericMethodParameter:
+ reader.ReadCompressedInteger ();
+ unresolvedTypeName = null;
+ unresolvedAssemblyName = null;
+ return true;
+ case SignatureTypeCode.SZArray:
+ return IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName);
+ case SignatureTypeCode.Array:
+ if (!IsResolvableSignatureType (ref reader, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) {
+ return false;
+ }
+ SkipArrayShape (ref reader);
+ unresolvedTypeName = null;
+ unresolvedAssemblyName = null;
+ return true;
+ 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;
+ }
+ 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:
+ // 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;
+ }
+ }
+
+ bool IsResolvableTypeDefOrRefEncodedHandle (
+ int encodedHandle,
+ AssemblyIndex index,
+ HashSet<(string AssemblyName, int TypeRow)> 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 ();
@@ -1636,13 +1886,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 95d0446b205..4d2d75a4620 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,10 @@ 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..8699b371bcd 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
@@ -43,6 +43,13 @@ 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 +142,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 +150,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..87af547d8b1 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;
@@ -34,6 +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 LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map.");
}
@@ -55,7 +66,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) },
+ [Input ("TestAssembly", peReader)],
new Version (11, 0),
new HashSet ());
Assert.Empty (result.GeneratedAssemblies);
@@ -67,7 +78,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 ([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 +89,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 ([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 +150,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 +160,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 ([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 +173,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 ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ());
foreach (var source in result.GeneratedJavaSources)
Assert.Contains ("class ", source.Content);
}
@@ -172,7 +183,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes ()
{
using var peReader = CreateTestFixturePEReader ();
var result = CreateGenerator ().Execute (
- new List<(string, PEReader)> { ("Mono.Android", peReader) },
+ [Input ("Mono.Android", peReader)],
new Version (11, 0),
new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" });
@@ -196,7 +207,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting ()
""");
var result = CreateGenerator ().Execute (
- new List<(string, PEReader)> { ("TestFixtures", peReader) },
+ [Input ("TestFixtures", peReader)],
new Version (11, 0),
new HashSet (),
useSharedTypemapUniverse: false,
@@ -212,11 +223,218 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting ()
Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution.");
}
+ [Theory]
+ [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 ();
+ var peerPath = Path.Combine (Path.GetTempPath (), "StalePeerAssembly.dll");
+ var missingDependencyPath = Path.Combine (Path.GetTempPath (), "MissingDependency.dll");
+ 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);
+
+ var result = CreateGenerator (warnings).Execute (
+ [
+ new AssemblyInput ("StalePeerAssembly", peerPath, peerReader),
+ new AssemblyInput ("MissingDependency", missingDependencyPath, 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 (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) =>
new (new TestTrimmableTypeMapLogger (logMessages, warnings));
+ static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader);
+
+ public enum StaleReferenceShape {
+ BaseType,
+ Interface,
+ GenericBaseArgument,
+ GenericConstraint,
+ }
+
+ 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 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 ();
+ 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",
+ StaleReferenceShape.GenericConstraint => "MissingConstraint",
+ _ => "MissingBase",
+ };
+ var missingDependencyRef = pe.FindOrAddAssemblyRef ("MissingDependency");
+ var missingTypeRef = pe.Metadata.AddTypeReference (
+ missingDependencyRef,
+ pe.Metadata.GetOrAddString ("MissingDependency"),
+ pe.Metadata.GetOrAddString (missingTypeName));
+ var objectRef = pe.Metadata.AddTypeReference (
+ pe.SystemRuntimeRef,
+ pe.Metadata.GetOrAddString ("System"),
+ pe.Metadata.GetOrAddString ("Object"));
+ EntityHandle peerBaseType = shape switch {
+ StaleReferenceShape.BaseType => missingTypeRef,
+ StaleReferenceShape.GenericBaseArgument => CreateGenericBaseTypeSpec (pe, objectRef, missingTypeRef),
+ _ => objectRef,
+ };
+
+ 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"),
+ peerBaseType,
+ 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 (shape == StaleReferenceShape.Interface) {
+ 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;
+ }
+
+ 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")]