diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index edb0171758f..78ae91c7cfb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -39,6 +39,7 @@ public TrimmableTypeMapResult Execute ( } RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + PropagateDeferredRegistrationToBaseClasses (allPeers); var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => @@ -207,6 +208,42 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } + /// + /// Propagates up the base class chain. + /// When a type like NUnitInstrumentation has deferred registration, its base class + /// TestInstrumentation_1 must also defer — otherwise the base class <clinit> will call + /// registerNatives before the managed runtime is ready. + /// + internal static void PropagateDeferredRegistrationToBaseClasses (List allPeers) + { + // In practice only 1–2 types need propagation (one Application, maybe one + // Instrumentation), each with a short base-class chain. A linear scan per + // ancestor is simpler and cheaper than building a Dictionary> + // lookup over all peers up front. + foreach (var peer in allPeers) { + if (peer.CannotRegisterInStaticConstructor) { + PropagateToAncestors (peer.BaseJavaName, allPeers); + } + } + + static void PropagateToAncestors (string? baseJniName, List allPeers) + { + while (baseJniName is not null) { + string? nextBase = null; + foreach (var basePeer in allPeers) { + if (!string.Equals (basePeer.JavaName, baseJniName, StringComparison.Ordinal) || basePeer.DoNotGenerateAcw) { + continue; + } + + basePeer.CannotRegisterInStaticConstructor = true; + nextBase = basePeer.BaseJavaName; + } + + baseJniName = nextBase; + } + } + } + static void AddPeerByDotName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) { if (!peersByDotName.TryGetValue (dotName, out var list)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 22bafb43711..c80fdbb6936 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -226,6 +226,52 @@ public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes Assert.True (peers [1].CannotRegisterInStaticConstructor, "Instrumentation type should defer Runtime.registerNatives()."); } + [Fact] + public void PropagateDeferredRegistrationToBaseClasses_PropagatesToBaseClassesOfManifestReferencedTypes () + { + var basePeer = new JavaPeerInfo { + JavaName = "crc64aaa/TestInstrumentation_1", CompatJniName = "crc64aaa/TestInstrumentation_1", + ManagedTypeName = "Tests.TestInstrumentation`1", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "TestInstrumentation`1", + AssemblyName = "Tests", IsUnconditional = false, + BaseJavaName = "android/app/Instrumentation", + }; + var midPeer = new JavaPeerInfo { + JavaName = "crc64bbb/NUnitTestInstrumentation", CompatJniName = "crc64bbb/NUnitTestInstrumentation", + ManagedTypeName = "Tests.NUnitTestInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitTestInstrumentation", + AssemblyName = "Tests", IsUnconditional = false, + BaseJavaName = "crc64aaa/TestInstrumentation_1", + }; + var leafPeer = new JavaPeerInfo { + JavaName = "crc64ccc/NUnitInstrumentation", CompatJniName = "crc64ccc/NUnitInstrumentation", + ManagedTypeName = "Tests.NUnitInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitInstrumentation", + AssemblyName = "Tests", IsUnconditional = false, + BaseJavaName = "crc64bbb/NUnitTestInstrumentation", + }; + var peers = new List { basePeer, midPeer, leafPeer }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + // RootManifestReferencedTypes sets the flag only on the directly matched leaf + Assert.True (leafPeer.CannotRegisterInStaticConstructor, "Leaf instrumentation should have deferred registration after manifest rooting."); + Assert.False (midPeer.CannotRegisterInStaticConstructor, "Mid peer should NOT have deferred registration before propagation."); + Assert.False (basePeer.CannotRegisterInStaticConstructor, "Base peer should NOT have deferred registration before propagation."); + + // PropagateDeferredRegistrationToBaseClasses walks the BaseJavaName chain + TrimmableTypeMapGenerator.PropagateDeferredRegistrationToBaseClasses (peers); + + Assert.True (leafPeer.CannotRegisterInStaticConstructor, "Leaf instrumentation should still have deferred registration."); + Assert.True (midPeer.CannotRegisterInStaticConstructor, "Mid peer should have deferred registration after propagation."); + Assert.True (basePeer.CannotRegisterInStaticConstructor, "Base peer should have deferred registration after propagation."); + } + [Fact] public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () {