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/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 f026f39ce71..7544d61b588 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -39,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 @@ -90,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); @@ -115,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 b4a17a52f3a..e184795495a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -23,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; @@ -233,9 +237,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; @@ -300,6 +312,242 @@ 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 typeRef = MetadataTypeNameResolver.GetTypeRefFromReference (index.Reader, handle, index.AssemblyName, rawTypeKind: 0); + var typeName = typeRef.ManagedTypeName; + var assemblyName = typeRef.AssemblyName; + 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 (); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 03fc2c35b13..1aca9a1993a 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/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 0615b52bf12..fefa1f4ad0a 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1614,6 +1614,15 @@ public static string XA4256 { } } + /// + /// 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 XA4257 { + get { + return ResourceManager.GetString("XA4257", 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 2ac6b39b8c6..a00748a7e49 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1171,6 +1171,15 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS {0} - MSBuild XML element name {1} - MSBuild XML element name {2} - MSBuild ItemSpec value + + + 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 b9f241144d9..2ca8f412a48 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -44,6 +44,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 ("XA4257", Properties.Resources.XA4257, 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 547936dd37b..0986f5b45d4 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."); } @@ -212,6 +223,83 @@ 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 XA4257 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) => @@ -219,6 +307,134 @@ TrimmableTypeMapGenerator CreateGenerator (List 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")]