From 59665858aee9eb40047d42f0b23cbdcc0878e5c7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 09:14:41 +0200 Subject: [PATCH 01/45] [typemap] Validate exported Java fields Match legacy XA4205 and XA4208 validation in the trimmable scanner, before invalid field members can reach generated typemap or Java outputs. Extend semantic, build, and device coverage for valid static and instance ExportField behavior and classify legacy-unsupported field names. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 2 + .../Scanner/JavaPeerScanner.cs | 27 ++++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 4 ++ .../TrimmableTypeMapBuildTests.cs | 65 +++++++++++++++++++ .../Generator/ExportFieldTests.cs | 15 +++++ .../TrimmableTypeMapGeneratorTests.cs | 21 ++++++ .../TestFixtures/TestTypes.cs | 14 ++++ .../Java.Interop/ExportTests.cs | 16 +++++ 8 files changed, 164 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index ae25273fa8a..7dfcc5eb22d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -22,6 +22,8 @@ void LogUnresolvableJavaPeerSkippedWarning ( string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); void LogInvalidJavaNameError (string javaName, string invalidIdentifier); + void LogExportFieldWithParametersError (); + void LogExportFieldReturnsVoidError (); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 989afeda295..187bb302d97 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -672,6 +672,10 @@ static void SkipArrayShape (ref BlobReader reader) foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!ValidateExportField (methodDef, index)) { + continue; + } + // Check for [ExportField] — produces both a marshal method AND a field CollectExportField (methodDef, index, fields); @@ -735,6 +739,29 @@ static void SkipArrayShape (ref BlobReader reader) return (methods, fields); } + bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index) + { + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + if (AssemblyIndex.GetCustomAttributeName (ca, index.Reader) != "ExportFieldAttribute") { + continue; + } + + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + if (sig.ParameterTypes.Length != 0) { + logger?.LogExportFieldWithParametersError (); + return false; + } + if (sig.ReturnType.ManagedTypeName == "System.Void") { + logger?.LogExportFieldReturnsVoidError (); + return false; + } + return true; + } + + return true; + } + static bool HasJniAddNativeMethodRegistrationAttribute (TypeDefinition typeDef, AssemblyIndex index) { const string JniAddNativeMethodRegistrationAttribute = "JniAddNativeMethodRegistrationAttribute"; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 1928954aad2..f8e8dbe1789 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -59,6 +59,10 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier); + public void LogExportFieldWithParametersError () => + log.LogCodedError ("XA4205", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); + public void LogExportFieldReturnsVoidError () => + log.LogCodedError ("XA4208", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4208); public void LogCustomJavaObjectError (string managedTypeName) => log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); public void LogCustomJavaObjectWarning (string managedTypeName) => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b5512e330fe..084b3908e2f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -36,6 +36,71 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value AssertTrimmableTypeMapOutputs (intermediateDir); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters", "XA4205")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "void", "XA4208")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "void", "XA4208")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "void", "XA4208")] + public void Build_InvalidExportField_ReportsLegacyDiagnostic ( + string typeMapImplementation, + AndroidRuntime runtime, + string invalidShape, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var initializer = invalidShape switch { + "parameters" => "public int InitialValue (int value) => value;", + "void" => "public void InitialValue () { }", + _ => throw new InvalidOperationException ($"Unknown invalid [ExportField] shape '{invalidShape}'."), + }; + var proj = CreateExportFieldValidationProject (runtime, typeMapImplementation, $""" + [ExportField ("VALUE")] + {initializer} + """); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape} [ExportField] initializers."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); + } + + static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( + AndroidRuntime runtime, + string typeMapImplementation, + string members) + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = runtime == AndroidRuntime.NativeAOT, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ExportFieldValidation.cs") { + TextContent = () => $$""" + using Android.Runtime; + using Java.Interop; + + namespace ExportFieldValidation { + [Register ("com/example/exportfields/ValidationPeer")] + class ValidationPeer : Java.Lang.Object { + public ValidationPeer () { + } + + {{members}} + } + } + """, + }); + return proj; + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 1f84df6b278..88755202423 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -51,4 +51,19 @@ public void JcwGenerator_EmitsInstanceFieldDeclarationAndMethodWrapper () Assert.Contains ("GetValue ()", java); Assert.Contains ("n_GetValue", java); } + + [Theory] + [InlineData ("my/app/ExportFieldWithParameter")] + [InlineData ("my/app/ExportFieldWithVoidReturn")] + public void ScannerAndGenerator_InvalidExportFieldProducesNoFieldOrMethod (string javaName) + { + var peer = FindFixtureByJavaName (javaName); + + Assert.Empty (peer.JavaFields); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); + + using var writer = new StringWriter (); + new JcwJavaSourceGenerator ().Generate (peer, writer); + Assert.DoesNotContain ("VALUE", writer.ToString ()); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 89075a5832d..9bd3eeff943 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -53,6 +53,10 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); + public void LogExportFieldWithParametersError () => + logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); + public void LogExportFieldReturnsVoidError () => + logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => @@ -340,6 +344,23 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () } } + [Fact] + public void Execute_InvalidExportFields_ReportLegacyDiagnosticsWithoutPartialMembers () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + + Assert.Contains (logMessages, message => message.StartsWith ("XA4205:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4208:", StringComparison.Ordinal)); + foreach (var javaName in new [] { "my/app/ExportFieldWithParameter", "my/app/ExportFieldWithVoidReturn" }) { + var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); + Assert.Empty (peer.JavaFields); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); + var source = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == javaName + ".java"); + Assert.DoesNotContain ("VALUE", source.Content); + } + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 5d0b6000196..873b25098ee 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -618,6 +618,20 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } + [Register ("my/app/ExportFieldWithParameter")] + public class ExportFieldWithParameter : Java.Lang.Object + { + [Java.Interop.ExportField ("VALUE")] + public int GetValue (int value) => value; + } + + [Register ("my/app/ExportFieldWithVoidReturn")] + public class ExportFieldWithVoidReturn : Java.Lang.Object + { + [Java.Interop.ExportField ("VALUE")] + public void GetValue () { } + } + [Register ("my/app/BaseInstrumentation")] public abstract class BaseInstrumentation : Android.App.Instrumentation { } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index 188a14cc191..39461a8fba2 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -305,6 +305,16 @@ public void ExportField_StaticObject_InitializerVisibleFromJava () Assert.AreEqual (43, value.IntValue ()); } } + + [Test, Category ("Export")] + public void ExportField_InstancePrimitive_InitializerVisibleFromJava () + { + using var value = new ExportInstanceField (); + using var klass = Java.Lang.Class.FromType (typeof (ExportInstanceField)); + var field = JNIEnv.GetFieldID (klass.Handle, "ANSWER", "I"); + Assert.AreNotEqual (IntPtr.Zero, field, "JNI field id for ANSWER not found"); + Assert.AreEqual (42, JNIEnv.GetIntField (value.Handle, field)); + } } // --------------------------------------------------------------- @@ -341,6 +351,12 @@ class ExportStaticField : Java.Lang.Object public static Java.Lang.Integer GetObjectAnswer () => new Java.Lang.Integer (43); } + class ExportInstanceField : Java.Lang.Object + { + [ExportField ("ANSWER")] + public int GetAnswer () => 42; + } + class ExportString : Java.Lang.Object { [Export] From e927a596f1b7db171e1b160b8a5e8c659bf846f7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 09:25:35 +0200 Subject: [PATCH 02/45] [tests] Cover combined ExportField validation Prove that parameter-count validation takes precedence over void-return validation for an initializer violating both rules, matching measured llvm-ir behavior across trimmable CoreCLR and NativeAOT without partial outputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/ExportFieldTests.cs | 1 + .../Generator/TrimmableTypeMapGeneratorTests.cs | 10 +++++++--- .../TestFixtures/TestTypes.cs | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 88755202423..6636a9f2985 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -55,6 +55,7 @@ public void JcwGenerator_EmitsInstanceFieldDeclarationAndMethodWrapper () [Theory] [InlineData ("my/app/ExportFieldWithParameter")] [InlineData ("my/app/ExportFieldWithVoidReturn")] + [InlineData ("my/app/ExportFieldWithParameterAndVoidReturn")] public void ScannerAndGenerator_InvalidExportFieldProducesNoFieldOrMethod (string javaName) { var peer = FindFixtureByJavaName (javaName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 9bd3eeff943..db9e3e6f2ec 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -350,9 +350,13 @@ public void Execute_InvalidExportFields_ReportLegacyDiagnosticsWithoutPartialMem using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); - Assert.Contains (logMessages, message => message.StartsWith ("XA4205:", StringComparison.Ordinal)); - Assert.Contains (logMessages, message => message.StartsWith ("XA4208:", StringComparison.Ordinal)); - foreach (var javaName in new [] { "my/app/ExportFieldWithParameter", "my/app/ExportFieldWithVoidReturn" }) { + Assert.Equal (2, logMessages.Count (message => message.StartsWith ("XA4205:", StringComparison.Ordinal))); + Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4208:", StringComparison.Ordinal))); + foreach (var javaName in new [] { + "my/app/ExportFieldWithParameter", + "my/app/ExportFieldWithVoidReturn", + "my/app/ExportFieldWithParameterAndVoidReturn", + }) { var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); Assert.Empty (peer.JavaFields); Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 873b25098ee..58ae0add037 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -632,6 +632,13 @@ public class ExportFieldWithVoidReturn : Java.Lang.Object public void GetValue () { } } + [Register ("my/app/ExportFieldWithParameterAndVoidReturn")] + public class ExportFieldWithParameterAndVoidReturn : Java.Lang.Object + { + [Java.Interop.ExportField ("VALUE")] + public void GetValue (int value) { } + } + [Register ("my/app/BaseInstrumentation")] public abstract class BaseInstrumentation : Android.App.Instrumentation { } From 0bf98e3b0ca83c4441da909eeb514fe0a44c9890 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 09:36:31 +0200 Subject: [PATCH 03/45] [typemap] Validate exported fields on generic types Match legacy XA4207 precedence for ExportField methods declared on generic types before parameter-count or void-return validation, and reject them before typemap or Java outputs are written. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 1 + .../Scanner/JavaPeerScanner.cs | 10 ++++++++-- .../Tasks/GenerateTrimmableTypeMap.cs | 2 ++ .../TrimmableTypeMapBuildTests.cs | 12 +++++++++--- .../Generator/ExportFieldTests.cs | 1 + .../Generator/TrimmableTypeMapGeneratorTests.cs | 4 ++++ .../TestFixtures/TestTypes.cs | 7 +++++++ 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 7dfcc5eb22d..879bf42aafc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -23,6 +23,7 @@ void LogUnresolvableJavaPeerSkippedWarning ( void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); void LogInvalidJavaNameError (string javaName, string invalidIdentifier); void LogExportFieldWithParametersError (); + void LogExportFieldOnGenericTypeError (); void LogExportFieldReturnsVoidError (); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 187bb302d97..55907938f98 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -667,12 +667,13 @@ static void SkipArrayShape (ref BlobReader reader) var methods = new List (); var fields = new List (); HashSet? registeredMethodKeys = detectBaseOverrides ? new (StringComparer.Ordinal) : null; + bool isGenericType = typeDef.GetGenericParameters ().Count > 0; // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - if (!ValidateExportField (methodDef, index)) { + if (!ValidateExportField (methodDef, index, isGenericType)) { continue; } @@ -739,7 +740,7 @@ static void SkipArrayShape (ref BlobReader reader) return (methods, fields); } - bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index) + bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool isGenericType) { foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); @@ -747,6 +748,11 @@ bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index) continue; } + if (isGenericType) { + logger?.LogExportFieldOnGenericTypeError (); + return false; + } + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); if (sig.ParameterTypes.Length != 0) { logger?.LogExportFieldWithParametersError (); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f8e8dbe1789..49cf22bad76 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -61,6 +61,8 @@ public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier); public void LogExportFieldWithParametersError () => log.LogCodedError ("XA4205", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); + public void LogExportFieldOnGenericTypeError () => + log.LogCodedError ("XA4207", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4207); public void LogExportFieldReturnsVoidError () => log.LogCodedError ("XA4208", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4208); public void LogCustomJavaObjectError (string managedTypeName) => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 084b3908e2f..86a2d9bd26c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -42,6 +42,9 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "void", "XA4208")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "void", "XA4208")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "void", "XA4208")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic", "XA4207")] public void Build_InvalidExportField_ReportsLegacyDiagnostic ( string typeMapImplementation, AndroidRuntime runtime, @@ -56,12 +59,13 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( var initializer = invalidShape switch { "parameters" => "public int InitialValue (int value) => value;", "void" => "public void InitialValue () { }", + "generic" => "public int InitialValue () => 42;", _ => throw new InvalidOperationException ($"Unknown invalid [ExportField] shape '{invalidShape}'."), }; var proj = CreateExportFieldValidationProject (runtime, typeMapImplementation, $""" [ExportField ("VALUE")] {initializer} - """); + """, genericType: invalidShape == "generic"); using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; @@ -72,8 +76,10 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( AndroidRuntime runtime, string typeMapImplementation, - string members) + string members, + bool genericType = false) { + var typeParameters = genericType ? "" : ""; var proj = new XamarinAndroidApplicationProject { IsRelease = runtime == AndroidRuntime.NativeAOT, References = { @@ -89,7 +95,7 @@ static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( namespace ExportFieldValidation { [Register ("com/example/exportfields/ValidationPeer")] - class ValidationPeer : Java.Lang.Object { + class ValidationPeer{{typeParameters}} : Java.Lang.Object { public ValidationPeer () { } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 6636a9f2985..924f6040901 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -56,6 +56,7 @@ public void JcwGenerator_EmitsInstanceFieldDeclarationAndMethodWrapper () [InlineData ("my/app/ExportFieldWithParameter")] [InlineData ("my/app/ExportFieldWithVoidReturn")] [InlineData ("my/app/ExportFieldWithParameterAndVoidReturn")] + [InlineData ("my/app/GenericExportField")] public void ScannerAndGenerator_InvalidExportFieldProducesNoFieldOrMethod (string javaName) { var peer = FindFixtureByJavaName (javaName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index db9e3e6f2ec..452fddab2cd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -57,6 +57,8 @@ public void LogExportFieldWithParametersError () => logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); public void LogExportFieldReturnsVoidError () => logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); + public void LogExportFieldOnGenericTypeError () => + logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => @@ -352,10 +354,12 @@ public void Execute_InvalidExportFields_ReportLegacyDiagnosticsWithoutPartialMem Assert.Equal (2, logMessages.Count (message => message.StartsWith ("XA4205:", StringComparison.Ordinal))); Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4208:", StringComparison.Ordinal))); + Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4207:", StringComparison.Ordinal))); foreach (var javaName in new [] { "my/app/ExportFieldWithParameter", "my/app/ExportFieldWithVoidReturn", "my/app/ExportFieldWithParameterAndVoidReturn", + "my/app/GenericExportField", }) { var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); Assert.Empty (peer.JavaFields); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 58ae0add037..72346a5384e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -639,6 +639,13 @@ public class ExportFieldWithParameterAndVoidReturn : Java.Lang.Object public void GetValue (int value) { } } + [Register ("my/app/GenericExportField")] + public class GenericExportField : Java.Lang.Object + { + [Java.Interop.ExportField ("VALUE")] + public int GetValue () => 42; + } + [Register ("my/app/BaseInstrumentation")] public abstract class BaseInstrumentation : Android.App.Instrumentation { } From 1daf1c6907b85a75247acb69861ffdf55f0db4bc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 09:46:33 +0200 Subject: [PATCH 04/45] [typemap] Match ExportField attributes by full name Ignore unrelated attributes that share the ExportFieldAttribute simple name by matching the Java.Interop namespace consistently during validation, field collection, and marshal-method registration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 12 ++++++++---- .../Generator/ExportFieldTests.cs | 17 +++++++++++++++++ .../Generator/TrimmableTypeMapGeneratorTests.cs | 6 ++++++ .../TestFixtures/StubAttributes.cs | 9 +++++++++ .../TestFixtures/TestTypes.cs | 7 +++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 55907938f98..4a4a14e8363 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -740,11 +740,16 @@ static void SkipArrayShape (ref BlobReader reader) return (methods, fields); } + static bool IsExportFieldAttribute (CustomAttribute attribute, AssemblyIndex index) + { + return AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Java.Interop", "ExportFieldAttribute"); + } + bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool isGenericType) { foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - if (AssemblyIndex.GetCustomAttributeName (ca, index.Reader) != "ExportFieldAttribute") { + if (!IsExportFieldAttribute (ca, index)) { continue; } @@ -1740,7 +1745,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, return true; } - if (attrName == "ExportFieldAttribute") { + if (IsExportFieldAttribute (ca, index)) { (registerInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); return true; } @@ -2644,9 +2649,8 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List method.ManagedMethodName == "GetValue"); + + using var writer = new StringWriter (); + new JcwJavaSourceGenerator ().Generate (peer, writer); + Assert.DoesNotContain ("NOT_A_FIELD", writer.ToString ()); + + var realPeer = FindFixtureByJavaName ("my/app/ExportFieldExample"); + Assert.Contains (realPeer.JavaFields, field => field.FieldName == "VALUE"); + Assert.Contains (realPeer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 452fddab2cd..71f60ecc8d2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -367,6 +367,12 @@ public void Execute_InvalidExportFields_ReportLegacyDiagnosticsWithoutPartialMem var source = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == javaName + ".java"); Assert.DoesNotContain ("VALUE", source.Content); } + + var lookalikePeer = result.AllPeers.Single (candidate => candidate.JavaName == "my/app/ExportFieldLookalike"); + Assert.Empty (lookalikePeer.JavaFields); + Assert.DoesNotContain (lookalikePeer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); + var lookalikeSource = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == "my/app/ExportFieldLookalike.java"); + Assert.DoesNotContain ("NOT_A_FIELD", lookalikeSource.Content); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 88ca5b2f78b..ab208d1926b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -10,6 +10,15 @@ public enum JniHandleOwnership } } +namespace Lookalike +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportFieldAttribute : Attribute + { + public ExportFieldAttribute (string name) { } + } +} + namespace Java.Interop { public struct JniObjectReference diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 72346a5384e..c7da068dce7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -646,6 +646,13 @@ public class GenericExportField : Java.Lang.Object public int GetValue () => 42; } + [Register ("my/app/ExportFieldLookalike")] + public class ExportFieldLookalike : Java.Lang.Object + { + [Lookalike.ExportField ("NOT_A_FIELD")] + public void GetValue (int value) { } + } + [Register ("my/app/BaseInstrumentation")] public abstract class BaseInstrumentation : Android.App.Instrumentation { } From 37b3a5459f2d2d61d5a1375e1ad24ec7a268bcf3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 14:54:04 +0200 Subject: [PATCH 05/45] [typemap] Use cached signature provider for ExportField validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 4a4a14e8363..164d4c9170d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -758,7 +758,7 @@ bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool return false; } - var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, index); if (sig.ParameterTypes.Length != 0) { logger?.LogExportFieldWithParametersError (); return false; From 5a3ce622e066077287078d16456e273d2c4b5a30 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 15:13:34 +0200 Subject: [PATCH 06/45] [tests] Reduce ExportField parity coverage Remove duplicated scanner fixtures and keep focused build/runtime parity coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/ExportFieldTests.cs | 34 ------------------ .../TrimmableTypeMapGeneratorTests.cs | 29 --------------- .../TestFixtures/StubAttributes.cs | 9 ----- .../TestFixtures/TestTypes.cs | 35 ------------------- 4 files changed, 107 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 1b13b675d19..1f84df6b278 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -51,38 +51,4 @@ public void JcwGenerator_EmitsInstanceFieldDeclarationAndMethodWrapper () Assert.Contains ("GetValue ()", java); Assert.Contains ("n_GetValue", java); } - - [Theory] - [InlineData ("my/app/ExportFieldWithParameter")] - [InlineData ("my/app/ExportFieldWithVoidReturn")] - [InlineData ("my/app/ExportFieldWithParameterAndVoidReturn")] - [InlineData ("my/app/GenericExportField")] - public void ScannerAndGenerator_InvalidExportFieldProducesNoFieldOrMethod (string javaName) - { - var peer = FindFixtureByJavaName (javaName); - - Assert.Empty (peer.JavaFields); - Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); - - using var writer = new StringWriter (); - new JcwJavaSourceGenerator ().Generate (peer, writer); - Assert.DoesNotContain ("VALUE", writer.ToString ()); - } - - [Fact] - public void ScannerAndGenerator_IgnoreLookalikeExportFieldAttribute () - { - var peer = FindFixtureByJavaName ("my/app/ExportFieldLookalike"); - - Assert.Empty (peer.JavaFields); - Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); - - using var writer = new StringWriter (); - new JcwJavaSourceGenerator ().Generate (peer, writer); - Assert.DoesNotContain ("NOT_A_FIELD", writer.ToString ()); - - var realPeer = FindFixtureByJavaName ("my/app/ExportFieldExample"); - Assert.Contains (realPeer.JavaFields, field => field.FieldName == "VALUE"); - Assert.Contains (realPeer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 71f60ecc8d2..b18904a7ec9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -346,35 +346,6 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () } } - [Fact] - public void Execute_InvalidExportFields_ReportLegacyDiagnosticsWithoutPartialMembers () - { - using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); - - Assert.Equal (2, logMessages.Count (message => message.StartsWith ("XA4205:", StringComparison.Ordinal))); - Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4208:", StringComparison.Ordinal))); - Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4207:", StringComparison.Ordinal))); - foreach (var javaName in new [] { - "my/app/ExportFieldWithParameter", - "my/app/ExportFieldWithVoidReturn", - "my/app/ExportFieldWithParameterAndVoidReturn", - "my/app/GenericExportField", - }) { - var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); - Assert.Empty (peer.JavaFields); - Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); - var source = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == javaName + ".java"); - Assert.DoesNotContain ("VALUE", source.Content); - } - - var lookalikePeer = result.AllPeers.Single (candidate => candidate.JavaName == "my/app/ExportFieldLookalike"); - Assert.Empty (lookalikePeer.JavaFields); - Assert.DoesNotContain (lookalikePeer.MarshalMethods, method => method.ManagedMethodName == "GetValue"); - var lookalikeSource = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == "my/app/ExportFieldLookalike.java"); - Assert.DoesNotContain ("NOT_A_FIELD", lookalikeSource.Content); - } - [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index ab208d1926b..88ca5b2f78b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -10,15 +10,6 @@ public enum JniHandleOwnership } } -namespace Lookalike -{ - [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] - public sealed class ExportFieldAttribute : Attribute - { - public ExportFieldAttribute (string name) { } - } -} - namespace Java.Interop { public struct JniObjectReference diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index c7da068dce7..5d0b6000196 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -618,41 +618,6 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } - [Register ("my/app/ExportFieldWithParameter")] - public class ExportFieldWithParameter : Java.Lang.Object - { - [Java.Interop.ExportField ("VALUE")] - public int GetValue (int value) => value; - } - - [Register ("my/app/ExportFieldWithVoidReturn")] - public class ExportFieldWithVoidReturn : Java.Lang.Object - { - [Java.Interop.ExportField ("VALUE")] - public void GetValue () { } - } - - [Register ("my/app/ExportFieldWithParameterAndVoidReturn")] - public class ExportFieldWithParameterAndVoidReturn : Java.Lang.Object - { - [Java.Interop.ExportField ("VALUE")] - public void GetValue (int value) { } - } - - [Register ("my/app/GenericExportField")] - public class GenericExportField : Java.Lang.Object - { - [Java.Interop.ExportField ("VALUE")] - public int GetValue () => 42; - } - - [Register ("my/app/ExportFieldLookalike")] - public class ExportFieldLookalike : Java.Lang.Object - { - [Lookalike.ExportField ("NOT_A_FIELD")] - public void GetValue (int value) { } - } - [Register ("my/app/BaseInstrumentation")] public abstract class BaseInstrumentation : Android.App.Instrumentation { } From fc3b0b863338c51e1e057c852c306e8f7f9e7a2c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 17:15:49 +0200 Subject: [PATCH 07/45] [tests] Cover ExportField diagnostic precedence Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapBuildTests.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 86a2d9bd26c..95cfe4443d3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -45,6 +45,12 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic", "XA4207")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic", "XA4207")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic", "XA4207")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters-and-void", "XA4205")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-parameters-and-void", "XA4207")] public void Build_InvalidExportField_ReportsLegacyDiagnostic ( string typeMapImplementation, AndroidRuntime runtime, @@ -60,17 +66,32 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( "parameters" => "public int InitialValue (int value) => value;", "void" => "public void InitialValue () { }", "generic" => "public int InitialValue () => 42;", + "parameters-and-void" => "public void InitialValue (int value) { }", + "generic-parameters-and-void" => "public void InitialValue (int value) { }", _ => throw new InvalidOperationException ($"Unknown invalid [ExportField] shape '{invalidShape}'."), }; var proj = CreateExportFieldValidationProject (runtime, typeMapImplementation, $""" [ExportField ("VALUE")] {initializer} - """, genericType: invalidShape == "generic"); + """, genericType: invalidShape.StartsWith ("generic", StringComparison.Ordinal)); using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape} [ExportField] initializers."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); + if (invalidShape == "parameters-and-void") { + Assert.IsFalse ( + builder.LastBuildOutput.Any (line => line.Contains ("error XA4208", StringComparison.Ordinal)), + "XA4205 should take precedence over XA4208, matching LLVM-IR." + ); + } else if (invalidShape == "generic-parameters-and-void") { + Assert.IsFalse ( + builder.LastBuildOutput.Any (line => + line.Contains ("error XA4205", StringComparison.Ordinal) || + line.Contains ("error XA4208", StringComparison.Ordinal)), + "XA4207 should take precedence over initializer signature diagnostics, matching LLVM-IR." + ); + } } static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( From 651fe84486543bb852279770744c0502a3f6c140 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 10:58:49 +0200 Subject: [PATCH 08/45] [typemap] Diagnose unsupported export signatures Reject unresolved managed, generic, and function-pointer export signatures during trimmable scanning with localized XA4263 diagnostics before any typemap, Java, or ACW-map output is written. Preserve legacy XA4206 precedence and supported Java mappings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/TOC.yml | 2 + Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4263.md | 24 +++ .../ITrimmableTypeMapLogger.cs | 2 + .../Scanner/JavaPeerScanner.cs | 114 ++++++++++- .../Properties/Resources.Designer.cs | 10 + .../Properties/Resources.resx | 6 + .../Tasks/GenerateTrimmableTypeMap.cs | 4 + .../TrimmableTypeMapBuildTests.cs | 180 ++++++++++++++++++ .../Generator/ExportSignatureTests.cs | 26 +++ .../TrimmableTypeMapGeneratorTests.cs | 29 +++ .../TestFixtures/TestTypes.cs | 46 +++++ 12 files changed, 437 insertions(+), 7 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4263.md create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 25edf535e06..4129d338fff 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -330,6 +330,8 @@ href: messages/xa4249.md - name: XA4258 href: messages/xa4258.md + - name: XA4263 + href: messages/xa4263.md - name: XA4301 href: messages/xa4301.md - name: XA4302 diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 8c4ea0192de..f50638f2f54 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -228,6 +228,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [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. + [XA4258](xa4258.md): Java name '{name}' contains reserved Java identifier '{identifier}'. Change the package or type name. ++ [XA4263](xa4263.md): The exported member '{member}' has unsupported signature type '{type}'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. + 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/xa4263.md b/Documentation/docs-mobile/messages/xa4263.md new file mode 100644 index 00000000000..392a7a7d8de --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4263.md @@ -0,0 +1,24 @@ +--- +title: .NET for Android error XA4263 +description: XA4263 error code +ms.date: 08/31/2026 +f1_keywords: + - "XA4263" +--- + +# .NET for Android error XA4263 + +## Example message + +```text +error XA4263: The exported member 'Peer.Get' has unsupported signature type 'ManagedValue'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. +``` + +## Issue + +An `[Export]` method or `[ExportField]` initializer uses a managed parameter or return type that has no Java type mapping. +The type cannot be represented safely in the generated Java declaration. + +## Solution + +Use a primitive, string, array, Java peer, or another supported managed-to-Java type. For `Stream` or `XmlReader`, apply `[ExportParameter]` with the appropriate Java mapping. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 879bf42aafc..edd36592f83 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -23,8 +23,10 @@ void LogUnresolvableJavaPeerSkippedWarning ( void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); void LogInvalidJavaNameError (string javaName, string invalidIdentifier); void LogExportFieldWithParametersError (); + void LogExportOnGenericTypeError (); void LogExportFieldOnGenericTypeError (); void LogExportFieldReturnsVoidError (); + void LogUnsupportedExportSignatureError (string memberName, string managedTypeName); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 164d4c9170d..fd1d530e85d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -676,6 +676,9 @@ static void SkipArrayShape (ref BlobReader reader) if (!ValidateExportField (methodDef, index, isGenericType)) { continue; } + if (!ValidateExportSignature (methodDef, index, isGenericType)) { + continue; + } // Check for [ExportField] — produces both a marshal method AND a field CollectExportField (methodDef, index, fields); @@ -773,6 +776,79 @@ bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool return true; } + bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, bool isGenericType) + { + bool isExport = false; + bool isExportField = false; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + if (attrName == "ExportAttribute") { + isExport = true; + } else if (IsExportFieldAttribute (ca, index)) { + isExportField = true; + } + } + if (!isExport && !isExportField) { + return true; + } + + var methodName = index.Reader.GetString (methodDef.Name); + if (methodName is ".ctor" or ".cctor") { + return true; + } + if (isExport && isGenericType) { + logger?.LogExportOnGenericTypeError (); + return false; + } + + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + var (parameterKinds, returnKind) = isExport + ? GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length) + : (CreateDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); + var declaringType = index.Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + string declaringTypeName = MetadataTypeNameResolver.GetFullName (declaringType, index.Reader); + string memberName = $"{declaringTypeName}.{methodName}"; + + for (int i = 0; i < sig.ParameterTypes.Length; i++) { + if (!HasExportSignatureMapping (sig.ParameterTypes [i], parameterKinds [i])) { + logger?.LogUnsupportedExportSignatureError (memberName, sig.ParameterTypes [i].DisplayName); + return false; + } + } + if (!HasExportSignatureMapping (sig.ReturnType, returnKind)) { + logger?.LogUnsupportedExportSignatureError (memberName, sig.ReturnType.DisplayName); + return false; + } + return true; + } + + bool HasExportSignatureMapping (TypeRefData managedType, ExportParameterKindInfo exportKind) + { + if (TryManagedTypeToJniDescriptor (managedType, exportKind, out _)) { + return true; + } + + // Legacy maps by-ref, pointer, and rectangular-array signatures through their + // element type. Their trimmable dispatch parity is tracked separately; do not + // turn llvm-ir-supported signatures into trimmable-only diagnostics here. + string managedTypeName = managedType.ManagedTypeName; + if (managedTypeName.EndsWith ("&", StringComparison.Ordinal) || + managedTypeName.EndsWith ("*", StringComparison.Ordinal)) { + return TryManagedTypeToJniDescriptor (managedType with { + ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 1), + }, exportKind, out _); + } + + int arrayStart = managedTypeName.LastIndexOf ('['); + if (arrayStart >= 0 && managedTypeName.EndsWith ("]", StringComparison.Ordinal)) { + return TryManagedTypeToJniDescriptor (managedType with { + ManagedTypeName = managedTypeName.Substring (0, arrayStart), + }, exportKind, out _); + } + return false; + } + static bool HasJniAddNativeMethodRegistrationAttribute (TypeDefinition typeDef, AssemblyIndex index) { const string JniAddNativeMethodRegistrationAttribute = "JniAddNativeMethodRegistrationAttribute"; @@ -1951,30 +2027,51 @@ string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnly /// for types that cannot be resolved (used by [Export] signature computation). /// string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindInfo exportKind = ExportParameterKindInfo.Unspecified) + { + return TryManagedTypeToJniDescriptor (managedType, exportKind, out var descriptor) + ? descriptor + : "Ljava/lang/Object;"; + } + + bool TryManagedTypeToJniDescriptor ( + TypeRefData managedType, + ExportParameterKindInfo exportKind, + out string descriptor) { if (exportKind != ExportParameterKindInfo.Unspecified) { - return exportKind switch { + descriptor = exportKind switch { ExportParameterKindInfo.InputStream => "Ljava/io/InputStream;", ExportParameterKindInfo.OutputStream => "Ljava/io/OutputStream;", ExportParameterKindInfo.XmlPullParser => "Lorg/xmlpull/v1/XmlPullParser;", ExportParameterKindInfo.XmlResourceParser => "Landroid/content/res/XmlResourceParser;", _ => "Ljava/lang/Object;", }; + return true; } var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); if (primitive is not null) { - return primitive; + descriptor = primitive; + return true; } if (managedType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { - return $"[{ManagedTypeToJniDescriptor (managedType with { ManagedTypeName = managedType.ManagedTypeName.Substring (0, managedType.ManagedTypeName.Length - 2) })}"; + var elementType = managedType with { + ManagedTypeName = managedType.ManagedTypeName.Substring (0, managedType.ManagedTypeName.Length - 2), + }; + if (TryManagedTypeToJniDescriptor (elementType, exportKind, out var elementDescriptor)) { + descriptor = $"[{elementDescriptor}"; + return true; + } + descriptor = ""; + return false; } // Try to resolve as a Java peer type with [Register] var resolved = TryResolveJniObjectDescriptor (managedType.ManagedTypeName); if (resolved is not null) { - return resolved; + descriptor = resolved; + return true; } // Well-known interface types that legacy CallbackCode mapped explicitly @@ -1990,17 +2087,20 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI _ => null, }; if (wellKnown is not null) { - return wellKnown; + descriptor = wellKnown; + return true; } // Enum parameters use their underlying primitive JNI ABI (matches legacy // CallbackCode behavior). var enumDescriptor = TryResolveEnumUnderlyingDescriptor (managedType.ManagedTypeName, managedType.AssemblyName); if (enumDescriptor is not null) { - return enumDescriptor; + descriptor = enumDescriptor; + return true; } - return "Ljava/lang/Object;"; + descriptor = ""; + return false; } /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 15af76356e0..961f3191ed7 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1659,6 +1659,16 @@ public static string XA4258 { } } + /// + /// Looks up a localized string similar to The exported member '{0}' has unsupported signature type '{1}'. + /// Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter].. + /// + public static string XA4263 { + get { + return ResourceManager.GetString("XA4263", 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 75a82d67971..d85b166adea 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1238,6 +1238,12 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS The following are literal names and should not be translated: Java. {0} - Java package or type name {1} - Java reserved keyword or restricted identifier + + + The exported member '{0}' has unsupported signature type '{1}'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. + The following are literal names and should not be translated: Java, [ExportParameter]. +{0} - Fully-qualified managed member name +{1} - Managed parameter or return 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 49cf22bad76..702ec1d221a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -61,10 +61,14 @@ public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier); public void LogExportFieldWithParametersError () => log.LogCodedError ("XA4205", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); + public void LogExportOnGenericTypeError () => + log.LogCodedError ("XA4206", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4206); public void LogExportFieldOnGenericTypeError () => log.LogCodedError ("XA4207", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4207); public void LogExportFieldReturnsVoidError () => log.LogCodedError ("XA4208", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4208); + public void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => + log.LogCodedError ("XA4263", Properties.Resources.XA4263, memberName, managedTypeName); public void LogCustomJavaObjectError (string managedTypeName) => log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); public void LogCustomJavaObjectWarning (string managedTypeName) => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 95cfe4443d3..9cfc9d2b557 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -94,6 +94,119 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( } } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-parameter", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-parameter", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-parameter", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-return", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-return", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-return", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-field", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-field", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-field", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-parameter", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-parameter", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-parameter", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-instantiation", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-instantiation", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-instantiation", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "function-pointer", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "function-pointer", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "function-pointer", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-declaring-type", "XA4206")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-declaring-type", "XA4206")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-declaring-type", "XA4206")] + public void Build_UnsupportedExportSignature_ReportsCodedDiagnosticWithoutPartialOutputs ( + string typeMapImplementation, + AndroidRuntime runtime, + string invalidShape, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var (additionalTypes, member, marker, typeParameters) = invalidShape switch { + "arbitrary-parameter" => ( + "public sealed class ManagedOnly { }", + """[Export ("unsupported")] public void UnsupportedMember (ManagedOnly value) { }""", + "unsupported", + ""), + "arbitrary-return" => ( + "public sealed class ManagedOnly { }", + """[Export ("unsupported")] public ManagedOnly UnsupportedMember () => new ();""", + "unsupported", + ""), + "arbitrary-field" => ( + "public sealed class ManagedOnly { }", + """[ExportField ("UNSUPPORTED_FIELD")] public ManagedOnly UnsupportedMember () => new ();""", + "UNSUPPORTED_FIELD", + ""), + "generic-parameter" => ( + "", + """[Export ("unsupported")] public T UnsupportedMember (T value) => value;""", + "unsupported", + ""), + "generic-instantiation" => ( + "", + """[Export ("unsupported")] public List UnsupportedMember (List value) => value;""", + "unsupported", + ""), + "function-pointer" => ( + "", + """[Export ("unsupported")] public unsafe delegate* unmanaged UnsupportedMember (delegate* unmanaged value) => value;""", + "unsupported", + ""), + "generic-declaring-type" => ( + "", + """[Export ("unsupported")] public int UnsupportedMember () => 0;""", + "unsupported", + ""), + _ => throw new InvalidOperationException ($"Unknown unsupported [Export] shape '{invalidShape}'."), + }; + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.SetProperty ("AllowUnsafeBlocks", "true"); + proj.Sources.Add (new BuildItem.Source ("ExportSignatureValidation.cs") { + TextContent = () => $$""" + using System.Collections.Generic; + using Android.Runtime; + using Java.Interop; + + namespace ExportSignatureValidation { + {{additionalTypes}} + + [Register ("com/example/exports/SignaturePeer")] + public class SignaturePeer{{typeParameters}} : Java.Lang.Object { + public SignaturePeer () { + } + + {{member}} + } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape}."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); + if (expectedCode == "XA4263") { + StringAssertEx.Contains ( + "ExportSignatureValidation.SignaturePeer.UnsupportedMember", + builder.LastBuildOutput, + "The diagnostic should identify the unsupported managed member." + ); + } + AssertNoExportOutputs (builder, marker); + } + static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( AndroidRuntime runtime, string typeMapImplementation, @@ -128,6 +241,73 @@ public ValidationPeer () { return proj; } + [Test] + public void FindExportOutputs_FindsEveryArtifactKind () + { + var root = Path.Combine (Root, "temp", TestName); + var typemapDirectory = Path.Combine (root, "typemap"); + var acwMapFile = Path.Combine (root, "acw-map.txt"); + var trimmableJavaDirectory = Path.Combine (typemapDirectory, "java", "com", "example"); + var llvmIrJavaDirectory = Path.Combine (root, "android", "src", "com", "example"); + Directory.CreateDirectory (trimmableJavaDirectory); + Directory.CreateDirectory (llvmIrJavaDirectory); + + var expected = new [] { + Path.Combine (typemapDirectory, "_Example.TypeMap.dll"), + Path.Combine (typemapDirectory, "_Microsoft.Android.TypeMaps.dll"), + acwMapFile, + Path.Combine (trimmableJavaDirectory, "TrimmablePeer.java"), + Path.Combine (llvmIrJavaDirectory, "LlvmIrPeer.java"), + }; + File.WriteAllBytes (expected [0], []); + File.WriteAllBytes (expected [1], []); + File.WriteAllText (expected [2], "Managed, Assembly;com/example/Peer"); + File.WriteAllText (expected [3], "public int VALUE = InitialValue ();"); + File.WriteAllText (expected [4], "public int VALUE = InitialValue ();"); + + CollectionAssert.AreEquivalent ( + expected, + FindExportOutputs (typemapDirectory, acwMapFile, Path.Combine (root, "android", "src"), "VALUE") + ); + } + + static void AssertNoExportOutputs (ProjectBuilder builder, string memberName) + { + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var acwMapFile = builder.Output.GetIntermediaryPath ("acw-map.txt"); + var androidSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src")); + var outputs = FindExportOutputs (typemapDirectory, acwMapFile, androidSourceDirectory, memberName); + Assert.IsEmpty ( + outputs, + "Invalid exported metadata should not produce typemap assemblies, ACW maps, or partial Java output:" + + Environment.NewLine + string.Join (Environment.NewLine, outputs) + ); + } + + static string [] FindExportOutputs (string typemapDirectory, string acwMapFile, string androidSourceDirectory, string memberName) + { + var outputs = new List (); + if (Directory.Exists (typemapDirectory)) { + outputs.AddRange (Directory.GetFiles (typemapDirectory, "*.TypeMap.dll", SearchOption.AllDirectories)); + outputs.AddRange (Directory.GetFiles (typemapDirectory, "_Microsoft.Android.TypeMaps.dll", SearchOption.AllDirectories)); + } + if (File.Exists (acwMapFile)) { + outputs.Add (acwMapFile); + } + + foreach (var javaDirectory in new [] { Path.Combine (typemapDirectory, "java"), androidSourceDirectory }) { + if (!Directory.Exists (javaDirectory)) { + continue; + } + foreach (var javaFile in Directory.GetFiles (javaDirectory, "*.java", SearchOption.AllDirectories)) { + if (File.ReadAllText (javaFile).Contains (memberName, StringComparison.Ordinal)) { + outputs.Add (javaFile); + } + } + } + return outputs.ToArray (); + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs new file mode 100644 index 00000000000..b092903f13f --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs @@ -0,0 +1,26 @@ +using System.IO; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ExportSignatureTests : FixtureTestBase +{ + [Theory] + [InlineData ("my/app/ExportWithUnsupportedManagedParameter")] + [InlineData ("my/app/ExportWithUnsupportedManagedReturn")] + [InlineData ("my/app/ExportFieldWithUnsupportedManagedReturn")] + [InlineData ("my/app/ExportWithGenericMethodParameter")] + [InlineData ("my/app/ExportWithGenericInstantiation")] + [InlineData ("my/app/GenericExportType")] + public void ScannerAndGenerator_UnsupportedExportSignatureProducesNoMember (string javaName) + { + var peer = FindFixtureByJavaName (javaName); + + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedMember"); + Assert.Empty (peer.JavaFields); + + using var writer = new StringWriter (); + new JcwJavaSourceGenerator ().Generate (peer, writer); + Assert.DoesNotContain (" unsupported (", writer.ToString (), System.StringComparison.Ordinal); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index b18904a7ec9..6ddbe33a648 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -55,10 +55,14 @@ public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); public void LogExportFieldWithParametersError () => logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); + public void LogExportOnGenericTypeError () => + logMessages.Add ("XA4206: [Export] cannot be used on a generic type."); public void LogExportFieldReturnsVoidError () => logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); public void LogExportFieldOnGenericTypeError () => logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); + public void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => + logMessages.Add ($"XA4263: The exported member '{memberName}' has unsupported signature type '{managedTypeName}'."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => @@ -346,6 +350,31 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () } } + [Fact] + public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPartialMembers () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + + Assert.Equal (5, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); + Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4206:", StringComparison.Ordinal))); + foreach (var javaName in new [] { + "my/app/ExportWithUnsupportedManagedParameter", + "my/app/ExportWithUnsupportedManagedReturn", + "my/app/ExportFieldWithUnsupportedManagedReturn", + "my/app/ExportWithGenericMethodParameter", + "my/app/ExportWithGenericInstantiation", + "my/app/GenericExportType", + }) { + var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedMember"); + Assert.Empty (peer.JavaFields); + var source = result.GeneratedJavaSources.Single (candidate => candidate.RelativePath == javaName + ".java"); + Assert.DoesNotContain (" unsupported (", source.Content, StringComparison.Ordinal); + Assert.DoesNotContain ("UNSUPPORTED_FIELD", source.Content, StringComparison.Ordinal); + } + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 5d0b6000196..2e70958ba64 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -580,6 +580,52 @@ public class ExportCollectionShapes : Java.Lang.Object public System.Collections.ICollection? EchoCollection (System.Collections.ICollection? value) => value; } + public class UnsupportedExportValue + { + } + + [Register ("my/app/ExportWithUnsupportedManagedParameter")] + public class ExportWithUnsupportedManagedParameter : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public void UnsupportedMember (UnsupportedExportValue value) { } + } + + [Register ("my/app/ExportWithUnsupportedManagedReturn")] + public class ExportWithUnsupportedManagedReturn : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public UnsupportedExportValue UnsupportedMember () => new (); + } + + [Register ("my/app/ExportFieldWithUnsupportedManagedReturn")] + public class ExportFieldWithUnsupportedManagedReturn : Java.Lang.Object + { + [Java.Interop.ExportField ("UNSUPPORTED_FIELD")] + public UnsupportedExportValue UnsupportedMember () => new (); + } + + [Register ("my/app/ExportWithGenericMethodParameter")] + public class ExportWithGenericMethodParameter : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public T UnsupportedMember (T value) => value; + } + + [Register ("my/app/ExportWithGenericInstantiation")] + public class ExportWithGenericInstantiation : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public System.Collections.Generic.List UnsupportedMember (System.Collections.Generic.List value) => value; + } + + [Register ("my/app/GenericExportType")] + public class GenericExportType : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public int UnsupportedMember () => 0; + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods. From ad85fd9e79947fc46949a69fd80d91a87502e9b2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 11:45:50 +0200 Subject: [PATCH 09/45] [typemap] Correct export signature validation Honor ExportParameter return mappings on exported fields, reject incompatible special mappings before generation, and resolve Java peer descriptors by assembly identity through type forwarders. Add semantic, cross-assembly, and three-runtime regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/xa4263.md | 4 +- .../Scanner/AssemblyIndex.cs | 26 +++- .../Scanner/JavaPeerScanner.cs | 134 ++++++++++++------ .../TrimmableTypeMapBuildTests.cs | 51 +++++++ .../ScannerExportShapesTests.cs | 25 +++- .../UserTypesFixture/UserTypes.cs | 25 ++++ .../Generator/ExportFieldTests.cs | 12 ++ .../Generator/ExportSignatureTests.cs | 3 + .../TrimmableTypeMapGeneratorTests.cs | 5 +- .../TestFixtures/TestTypes.cs | 34 +++++ 10 files changed, 271 insertions(+), 48 deletions(-) diff --git a/Documentation/docs-mobile/messages/xa4263.md b/Documentation/docs-mobile/messages/xa4263.md index 392a7a7d8de..3da646d645d 100644 --- a/Documentation/docs-mobile/messages/xa4263.md +++ b/Documentation/docs-mobile/messages/xa4263.md @@ -18,7 +18,9 @@ error XA4263: The exported member 'Peer.Get' has unsupported signature type 'Man An `[Export]` method or `[ExportField]` initializer uses a managed parameter or return type that has no Java type mapping. The type cannot be represented safely in the generated Java declaration. +This error also occurs when `[ExportParameter]` specifies a Java mapping that is incompatible with the managed type. ## Solution -Use a primitive, string, array, Java peer, or another supported managed-to-Java type. For `Stream` or `XmlReader`, apply `[ExportParameter]` with the appropriate Java mapping. +Use a primitive, string, array, Java peer, or another supported managed-to-Java type. +Apply input/output stream mappings only to `Stream`, and XML parser mappings only to `XmlReader`. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 30ca13c1b06..b0182877d6a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -30,6 +30,7 @@ sealed class AssemblyIndex : IDisposable public MetadataReader Reader { get; } public string AssemblyName { get; } + public string MetadataAssemblyName { get; } public string AssemblyPath { get; } internal TypeRefSignatureTypeProvider TypeRefSignatureProvider { get; } @@ -53,6 +54,11 @@ sealed class AssemblyIndex : IDisposable /// public HashSet ExportedTypeNames { get; } = new (StringComparer.Ordinal); + /// + /// Maps forwarded managed type names to the target assembly simple name. + /// + public Dictionary ForwardedTypeAssemblies { get; } = new (StringComparer.Ordinal); + /// /// True iff the assembly's metadata mentions /// Java.Interop.JniAddNativeMethodRegistrationAttribute (as a @@ -77,6 +83,7 @@ sealed class AssemblyIndex : IDisposable valueTypeReferences = new TypeRefData? [reader.TypeReferences.Count + 1]; Reader = reader; AssemblyName = assemblyName; + MetadataAssemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); AssemblyPath = assemblyPath; TypeRefSignatureProvider = new TypeRefSignatureTypeProvider (this); } @@ -107,7 +114,12 @@ void Build () foreach (var exportedTypeHandle in Reader.ExportedTypes) { var exportedType = Reader.GetExportedType (exportedTypeHandle); - ExportedTypeNames.Add (GetExportedTypeFullName (exportedType)); + var fullName = GetExportedTypeFullName (exportedType); + ExportedTypeNames.Add (fullName); + var forwardedAssemblyName = GetForwardedAssemblyName (exportedType); + if (forwardedAssemblyName is not null) { + ForwardedTypeAssemblies [fullName] = forwardedAssemblyName; + } } foreach (var typeHandle in Reader.TypeDefinitions) { @@ -146,6 +158,18 @@ string GetExportedTypeFullName (ExportedType exportedType) var ns = Reader.GetString (exportedType.Namespace); return MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); } + + string? GetForwardedAssemblyName (ExportedType exportedType) + { + if (exportedType.Implementation.Kind == HandleKind.AssemblyReference) { + var assemblyReference = Reader.GetAssemblyReference ((AssemblyReferenceHandle) exportedType.Implementation); + return Reader.GetString (assemblyReference.Name); + } + if (exportedType.Implementation.Kind == HandleKind.ExportedType) { + return GetForwardedAssemblyName (Reader.GetExportedType ((ExportedTypeHandle) exportedType.Implementation)); + } + return null; + } } (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fd1d530e85d..2746b5a6435 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -61,15 +61,36 @@ internal JavaPeerScanner (string? packageNamingPolicy, ITrimmableTypeMapLogger? /// bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) { - if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && - resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { - return true; + var visitedAssemblies = new HashSet (StringComparer.Ordinal); + while (visitedAssemblies.Add (assemblyName) && + TryGetAssemblyIndex (assemblyName, out resolvedIndex)) { + if (resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { + return true; + } + if (!resolvedIndex.ForwardedTypeAssemblies.TryGetValue (typeName, out assemblyName)) { + break; + } } handle = default; resolvedIndex = null; return false; } + bool TryGetAssemblyIndex (string assemblyName, [NotNullWhen (true)] out AssemblyIndex? index) + { + if (assemblyCache.TryGetValue (assemblyName, out index)) { + return true; + } + foreach (var candidate in assemblyCache.Values) { + if (string.Equals (candidate.MetadataAssemblyName, assemblyName, StringComparison.Ordinal)) { + index = candidate; + return true; + } + } + index = null; + return false; + } + /// /// Resolves the type that declares the native n_* callback. When the [Register] connector /// names a declaring type (e.g. an *Invoker in another assembly) that type is used; @@ -803,9 +824,7 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b } var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); - var (parameterKinds, returnKind) = isExport - ? GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length) - : (CreateDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); + var (parameterKinds, returnKind) = GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length); var declaringType = index.Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); string declaringTypeName = MetadataTypeNameResolver.GetFullName (declaringType, index.Reader); string memberName = $"{declaringTypeName}.{methodName}"; @@ -1197,32 +1216,47 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, } /// - /// Looks up a managed type name across loaded assemblies. If the type has + /// Looks up a managed type in its referenced assembly. If the type has /// [Register], returns "L<jniName>;". Otherwise returns null. /// + string? TryResolveJniObjectDescriptor (TypeRefData managedType) + { + if (!TryResolveType (managedType.ManagedTypeName, managedType.AssemblyName, out var handle, out var index)) { + return null; + } + return GetJniObjectDescriptor (handle, index); + } + + /// + /// Looks up an unqualified managed type name across loaded assemblies. + /// Used only by legacy-compatible constructor signature discovery. + /// string? TryResolveJniObjectDescriptor (string managedType) { foreach (var index in assemblyCache.Values) { if (index.TypesByFullName.TryGetValue (managedType, out var handle)) { - if (index.RegisterInfoByType.TryGetValue (handle, out var registerInfo)) { - return $"L{registerInfo.JniName};"; - } - - // User peer types (extend a Java peer but lack [Register]) - // get a CRC64-based JNI name in ScanAssembly. Mirror that here - // so [Export]/[ExportField] signatures referring to such types - // emit the correct peer descriptor instead of falling back to - // java/lang/Object. - var typeDef = index.Reader.GetTypeDefinition (handle); - if (ExtendsJavaPeer (handle, typeDef, index)) { - var (jniName, _) = ComputeAutoJniNames (typeDef, index); - return $"L{jniName};"; - } + return GetJniObjectDescriptor (handle, index); } } return null; } + string? GetJniObjectDescriptor (TypeDefinitionHandle handle, AssemblyIndex index) + { + if (index.RegisterInfoByType.TryGetValue (handle, out var registerInfo)) { + return $"L{registerInfo.JniName};"; + } + + // User peer types (extend a Java peer but lack [Register]) get a CRC64-based + // JNI name in ScanAssembly. Mirror that for exported signatures. + var typeDef = index.Reader.GetTypeDefinition (handle); + if (ExtendsJavaPeer (typeDef, index)) { + var (jniName, _) = ComputeAutoJniNames (typeDef, index); + return $"L{jniName};"; + } + return null; + } + /// /// Resolves a `typeof(X)` argument captured as an assembly-qualified name /// (e.g. "Java.IO.IOException, Mono.Android, ...") to its JNI internal @@ -2013,11 +2047,18 @@ string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnly { var managedName = index.Reader.GetString (methodDef.Name); var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, index); - var jniSig = BuildJniSignatureFromManaged (sig, GetDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); + var (parameterKinds, returnKind) = GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length); + var jniSig = BuildJniSignatureFromManaged (sig, parameterKinds, returnKind); return ( new RegisterInfo { JniName = managedName, Signature = jniSig, Connector = "__export__", DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = null, SuperArgumentsString = null, IsField = true } + new ExportInfo { + ThrownNames = null, + SuperArgumentsString = null, + IsField = true, + ParameterKinds = parameterKinds, + ReturnKind = returnKind, + } ); } @@ -2038,23 +2079,6 @@ bool TryManagedTypeToJniDescriptor ( ExportParameterKindInfo exportKind, out string descriptor) { - if (exportKind != ExportParameterKindInfo.Unspecified) { - descriptor = exportKind switch { - ExportParameterKindInfo.InputStream => "Ljava/io/InputStream;", - ExportParameterKindInfo.OutputStream => "Ljava/io/OutputStream;", - ExportParameterKindInfo.XmlPullParser => "Lorg/xmlpull/v1/XmlPullParser;", - ExportParameterKindInfo.XmlResourceParser => "Landroid/content/res/XmlResourceParser;", - _ => "Ljava/lang/Object;", - }; - return true; - } - - var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); - if (primitive is not null) { - descriptor = primitive; - return true; - } - if (managedType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { var elementType = managedType with { ManagedTypeName = managedType.ManagedTypeName.Substring (0, managedType.ManagedTypeName.Length - 2), @@ -2067,8 +2091,18 @@ bool TryManagedTypeToJniDescriptor ( return false; } + if (exportKind != ExportParameterKindInfo.Unspecified) { + return TryGetExportParameterDescriptor (managedType.ManagedTypeName, exportKind, out descriptor); + } + + var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); + if (primitive is not null) { + descriptor = primitive; + return true; + } + // Try to resolve as a Java peer type with [Register] - var resolved = TryResolveJniObjectDescriptor (managedType.ManagedTypeName); + var resolved = TryResolveJniObjectDescriptor (managedType); if (resolved is not null) { descriptor = resolved; return true; @@ -2103,6 +2137,21 @@ bool TryManagedTypeToJniDescriptor ( return false; } + static bool TryGetExportParameterDescriptor ( + string managedTypeName, + ExportParameterKindInfo exportKind, + out string descriptor) + { + descriptor = exportKind switch { + ExportParameterKindInfo.InputStream when managedTypeName == "System.IO.Stream" => "Ljava/io/InputStream;", + ExportParameterKindInfo.OutputStream when managedTypeName == "System.IO.Stream" => "Ljava/io/OutputStream;", + ExportParameterKindInfo.XmlPullParser when managedTypeName == "System.Xml.XmlReader" => "Lorg/xmlpull/v1/XmlPullParser;", + ExportParameterKindInfo.XmlResourceParser when managedTypeName == "System.Xml.XmlReader" => "Landroid/content/res/XmlResourceParser;", + _ => "", + }; + return descriptor.Length > 0; + } + /// /// Returns the JNI descriptor for primitive types and System.String. /// Returns null for all other types. @@ -2766,7 +2815,8 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List 0;""", "unsupported", ""), + "mismatched-export-parameter" => ( + "public sealed class ManagedOnly { }", + """ + [Export ("unsupported")] + public ManagedOnly UnsupportedMember ( + [ExportParameter (ExportParameterKind.InputStream)] ManagedOnly value) + => value; + """, + "unsupported", + ""), + "generic-export-parameter" => ( + "", + """ + [Export ("unsupported")] + public T UnsupportedMember ( + [ExportParameter (ExportParameterKind.InputStream)] T value) + => value; + """, + "unsupported", + ""), + "function-pointer-export-parameter" => ( + "", + """ + [Export ("unsupported")] + public unsafe delegate* unmanaged UnsupportedMember ( + [ExportParameter (ExportParameterKind.InputStream)] delegate* unmanaged value) + => value; + """, + "unsupported", + ""), + "mismatched-field-export-parameter" => ( + "public sealed class ManagedOnly { }", + """ + [return: ExportParameter (ExportParameterKind.OutputStream)] + [ExportField ("UNSUPPORTED_FIELD")] + public ManagedOnly UnsupportedMember () => new (); + """, + "UNSUPPORTED_FIELD", + ""), _ => throw new InvalidOperationException ($"Unknown unsupported [Export] shape '{invalidShape}'."), }; var proj = new XamarinAndroidApplicationProject { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs index 04d9f4eb1cf..5ccddc99ad0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs @@ -32,12 +32,14 @@ static MarshalMethodInfo[] GetMarshalMethods (string javaName) return GetPeer (javaName).MarshalMethods.ToArray (); } - static JavaPeerInfo GetPeer (string javaName) + static JavaPeerInfo GetPeer (string javaName) => GetPeer (javaName, UserTypesFixturePath); + + static JavaPeerInfo GetPeer (string javaName, params string [] fixturePaths) { - var fixturePath = UserTypesFixturePath; + var fixturePath = fixturePaths [0]; var dir = AssertNotNull (Path.GetDirectoryName (fixturePath)); - var paths = new System.Collections.Generic.List { fixturePath }; + var paths = new System.Collections.Generic.List (fixturePaths); var monoAndroid = Path.Combine (dir, "Mono.Android.dll"); var javaInterop = Path.Combine (dir, "Java.Interop.dll"); if (File.Exists (monoAndroid)) @@ -104,6 +106,23 @@ public void ExportField_RegistersGetterAsMarshalMethod () AssertHasExport (methods, "GetValue", "()Ljava/lang/String;"); AssertHasExport (methods, "GetCount", "()I"); + AssertHasExport (methods, "GetOutputStream", "()Ljava/io/OutputStream;"); + AssertHasExport (methods, "GetXmlParser", "()Lorg/xmlpull/v1/XmlPullParser;"); + + var peer = GetPeer ("ExportFieldShapes"); + Assert.Contains (peer.JavaFields, field => field.FieldName == "OUTPUT_STREAM" && field.JavaTypeName == "java.io.OutputStream"); + Assert.Contains (peer.JavaFields, field => field.FieldName == "XML_PARSER" && field.JavaTypeName == "org.xmlpull.v1.XmlPullParser"); + } + + [Fact] + public void Export_CrossAssemblySameManagedName_DoesNotBorrowPeerDescriptor () + { + var directory = AssertNotNull (Path.GetDirectoryName (UserTypesFixturePath)); + var javaSourceFixturePath = Path.Combine (directory, "JavaSourceParityFixture.dll"); + Assert.True (File.Exists (javaSourceFixturePath), $"JavaSourceParityFixture.dll not found at '{javaSourceFixturePath}'."); + + var peer = GetPeer ("ExportAssemblyCollisionShape", javaSourceFixturePath, UserTypesFixturePath); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedCollision"); } // === Phase A: dispatch & declaration shapes === diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index df25fc4c28e..a522c81c155 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -209,6 +209,14 @@ protected ExportFieldShapes (IntPtr handle, JniHandleOwnership transfer) : base [ExportField ("COUNT")] public int GetCount () => 0; + + [return: ExportParameter (ExportParameterKind.OutputStream)] + [ExportField ("OUTPUT_STREAM")] + public System.IO.Stream? GetOutputStream () => null; + + [return: ExportParameter (ExportParameterKind.XmlPullParser)] + [ExportField ("XML_PARSER")] + public System.Xml.XmlReader? GetXmlParser () => null; } // [ExportParameter] overrides a Stream / XmlReader's Java type without @@ -394,3 +402,20 @@ public class ExportInterfaceRenameShape : Java.Lang.Object, Android.Views.View.I public void OnClick (Android.Views.View? v) { } } } + +namespace UserApp.JavaSourceParity +{ + public class SignatureCollision + { + } +} + +namespace UserApp +{ + [Register ("com/example/userapp/ExportAssemblyCollisionShape")] + public class ExportAssemblyCollisionShape : Java.Lang.Object + { + [Export ("unsupportedCollision")] + public void UnsupportedCollision (JavaSourceParity.SignatureCollision value) { } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 1f84df6b278..1a22a78036e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -25,6 +25,18 @@ public void Scanner_DetectsExportFieldsWithCorrectProperties () Assert.False (instanceField.IsStatic); Assert.Equal ("GetValue", instanceField.InitializerMethodName); Assert.Equal ("java.lang.String", instanceField.JavaTypeName); + + var streamField = peer.JavaFields.First (f => f.FieldName == "OUTPUT_STREAM"); + Assert.Equal ("java.io.OutputStream", streamField.JavaTypeName); + var streamMethod = peer.MarshalMethods.First (m => m.ManagedMethodName == "GetOutputStream"); + Assert.Equal ("()Ljava/io/OutputStream;", streamMethod.JniSignature); + Assert.Equal (ExportParameterKindInfo.OutputStream, streamMethod.ManagedReturnExportKind); + + var xmlField = peer.JavaFields.First (f => f.FieldName == "XML_PARSER"); + Assert.Equal ("org.xmlpull.v1.XmlPullParser", xmlField.JavaTypeName); + var xmlMethod = peer.MarshalMethods.First (m => m.ManagedMethodName == "GetXmlParser"); + Assert.Equal ("()Lorg/xmlpull/v1/XmlPullParser;", xmlMethod.JniSignature); + Assert.Equal (ExportParameterKindInfo.XmlPullParser, xmlMethod.ManagedReturnExportKind); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs index b092903f13f..bfd889fd450 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs @@ -11,6 +11,9 @@ public class ExportSignatureTests : FixtureTestBase [InlineData ("my/app/ExportFieldWithUnsupportedManagedReturn")] [InlineData ("my/app/ExportWithGenericMethodParameter")] [InlineData ("my/app/ExportWithGenericInstantiation")] + [InlineData ("my/app/ExportWithInvalidExportParameterType")] + [InlineData ("my/app/ExportWithGenericExportParameter")] + [InlineData ("my/app/ExportFieldWithInvalidExportParameterType")] [InlineData ("my/app/GenericExportType")] public void ScannerAndGenerator_UnsupportedExportSignatureProducesNoMember (string javaName) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 6ddbe33a648..6a778527638 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -356,7 +356,7 @@ public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPar using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); - Assert.Equal (5, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); + Assert.Equal (8, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4206:", StringComparison.Ordinal))); foreach (var javaName in new [] { "my/app/ExportWithUnsupportedManagedParameter", @@ -364,6 +364,9 @@ public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPar "my/app/ExportFieldWithUnsupportedManagedReturn", "my/app/ExportWithGenericMethodParameter", "my/app/ExportWithGenericInstantiation", + "my/app/ExportWithInvalidExportParameterType", + "my/app/ExportWithGenericExportParameter", + "my/app/ExportFieldWithInvalidExportParameterType", "my/app/GenericExportType", }) { var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 2e70958ba64..d749680077b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -619,6 +619,32 @@ public class ExportWithGenericInstantiation : Java.Lang.Object public System.Collections.Generic.List UnsupportedMember (System.Collections.Generic.List value) => value; } + [Register ("my/app/ExportWithInvalidExportParameterType")] + public class ExportWithInvalidExportParameterType : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public UnsupportedExportValue UnsupportedMember ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedExportValue value) + => value; + } + + [Register ("my/app/ExportWithGenericExportParameter")] + public class ExportWithGenericExportParameter : Java.Lang.Object + { + [Java.Interop.Export ("unsupported")] + public T UnsupportedMember ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] T value) + => value; + } + + [Register ("my/app/ExportFieldWithInvalidExportParameterType")] + public class ExportFieldWithInvalidExportParameterType : Java.Lang.Object + { + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.OutputStream)] + [Java.Interop.ExportField ("UNSUPPORTED_FIELD")] + public UnsupportedExportValue UnsupportedMember () => new (); + } + [Register ("my/app/GenericExportType")] public class GenericExportType : Java.Lang.Object { @@ -662,6 +688,14 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base [Java.Interop.ExportField ("VALUE")] [JavaAnnotation] public string GetValue () => ""; + + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.OutputStream)] + [Java.Interop.ExportField ("OUTPUT_STREAM")] + public Stream? GetOutputStream () => null; + + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.XmlPullParser)] + [Java.Interop.ExportField ("XML_PARSER")] + public XmlReader? GetXmlParser () => null; } [Register ("my/app/BaseInstrumentation")] From e3556a03d22ce6bb498e1779e546b103c24f8c36 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 12:18:19 +0200 Subject: [PATCH 10/45] [typemap] Validate scalar export mappings Require special ExportParameter mappings to target exact scalar Stream or XmlReader types, and match Export, ExportParameter, and ExportField attributes by full Java.Interop identity throughout scanner validation and collection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/xa4263.md | 1 + .../Scanner/AssemblyIndex.cs | 2 - .../Scanner/JavaPeerScanner.cs | 31 +++++++--- .../TrimmableTypeMapBuildTests.cs | 62 ++++++++++++++++++- .../Generator/ExportSignatureTests.cs | 18 ++++++ .../TestFixtures/StubAttributes.cs | 21 +++++++ .../TestFixtures/TestTypes.cs | 15 +++++ 7 files changed, 136 insertions(+), 14 deletions(-) diff --git a/Documentation/docs-mobile/messages/xa4263.md b/Documentation/docs-mobile/messages/xa4263.md index 3da646d645d..c672ebf7228 100644 --- a/Documentation/docs-mobile/messages/xa4263.md +++ b/Documentation/docs-mobile/messages/xa4263.md @@ -24,3 +24,4 @@ This error also occurs when `[ExportParameter]` specifies a Java mapping that is Use a primitive, string, array, Java peer, or another supported managed-to-Java type. Apply input/output stream mappings only to `Stream`, and XML parser mappings only to `XmlReader`. +Do not apply these scalar mappings to arrays or other wrappers around those types. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index b0182877d6a..30e340ef367 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -197,8 +197,6 @@ string GetExportedTypeFullName (ExportedType exportedType) registerInfo = registerInfo with { JniName = registerInfo.JniName.Replace ('.', '/') }; } else if (attrName == "JniTypeSignatureAttribute") { registerInfo = ParseJniTypeSignatureAttribute (ca); - } else if (attrName == "ExportAttribute") { - // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner } else if (IsKnownComponentAttribute (attrName)) { attrInfo ??= CreateTypeAttributeInfo (attrName); var value = DecodeAttribute (ca); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 2746b5a6435..037076ea719 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -769,6 +769,16 @@ static bool IsExportFieldAttribute (CustomAttribute attribute, AssemblyIndex ind return AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Java.Interop", "ExportFieldAttribute"); } + static bool IsExportAttribute (CustomAttribute attribute, AssemblyIndex index) + { + return AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Java.Interop", "ExportAttribute"); + } + + static bool IsExportParameterAttribute (CustomAttribute attribute, AssemblyIndex index) + { + return AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Java.Interop", "ExportParameterAttribute"); + } + bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool isGenericType) { foreach (var caHandle in methodDef.GetCustomAttributes ()) { @@ -803,8 +813,7 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b bool isExportField = false; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); - if (attrName == "ExportAttribute") { + if (IsExportAttribute (ca, index)) { isExport = true; } else if (IsExportFieldAttribute (ca, index)) { isExportField = true; @@ -847,6 +856,9 @@ bool HasExportSignatureMapping (TypeRefData managedType, ExportParameterKindInfo if (TryManagedTypeToJniDescriptor (managedType, exportKind, out _)) { return true; } + if (exportKind != ExportParameterKindInfo.Unspecified) { + return false; + } // Legacy maps by-ref, pointer, and rectangular-array signatures through their // element type. Their trimmable dispatch parity is tracked separately; do not @@ -1850,7 +1862,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, return true; } - if (attrName == "ExportAttribute") { + if (IsExportAttribute (ca, index)) { (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); return true; } @@ -1994,8 +2006,7 @@ static ExportParameterKindInfo GetExportParameterKind (Parameter parameter, Asse { foreach (var caHandle in parameter.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = index.GetCustomAttributeName (ca); - if (attrName != "ExportParameterAttribute") { + if (!IsExportParameterAttribute (ca, index)) { continue; } @@ -2079,11 +2090,15 @@ bool TryManagedTypeToJniDescriptor ( ExportParameterKindInfo exportKind, out string descriptor) { + if (exportKind != ExportParameterKindInfo.Unspecified) { + return TryGetExportParameterDescriptor (managedType.ManagedTypeName, exportKind, out descriptor); + } + if (managedType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { var elementType = managedType with { ManagedTypeName = managedType.ManagedTypeName.Substring (0, managedType.ManagedTypeName.Length - 2), }; - if (TryManagedTypeToJniDescriptor (elementType, exportKind, out var elementDescriptor)) { + if (TryManagedTypeToJniDescriptor (elementType, ExportParameterKindInfo.Unspecified, out var elementDescriptor)) { descriptor = $"[{elementDescriptor}"; return true; } @@ -2091,10 +2106,6 @@ bool TryManagedTypeToJniDescriptor ( return false; } - if (exportKind != ExportParameterKindInfo.Unspecified) { - return TryGetExportParameterDescriptor (managedType.ManagedTypeName, exportKind, out descriptor); - } - var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); if (primitive is not null) { descriptor = primitive; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 0d2085e5c8f..835bae51806 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -127,7 +127,19 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "mismatched-field-export-parameter", "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "mismatched-field-export-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "mismatched-field-export-parameter", "XA4263")] - public void Build_UnsupportedExportSignature_ReportsCodedDiagnosticWithoutPartialOutputs ( + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-parameter", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-parameter", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-parameter", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-return", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-return", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-return", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-field", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-field", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-field", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-xml-array-return", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-xml-array-return", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-xml-array-return", "XA4263")] + public void Build_ExportSignature_MatchesRuntimeClassification ( string typeMapImplementation, AndroidRuntime runtime, string invalidShape, @@ -213,6 +225,44 @@ public T UnsupportedMember ( """, "UNSUPPORTED_FIELD", ""), + "special-array-parameter" => ( + "", + """ + [Export ("unsupported")] + public void UnsupportedMember ( + [ExportParameter (ExportParameterKind.InputStream)] Stream [] value) + { + } + """, + "unsupported", + ""), + "special-array-return" => ( + "", + """ + [return: ExportParameter (ExportParameterKind.OutputStream)] + [Export ("unsupported")] + public Stream [] UnsupportedMember () => []; + """, + "unsupported", + ""), + "special-array-field" => ( + "", + """ + [return: ExportParameter (ExportParameterKind.OutputStream)] + [ExportField ("UNSUPPORTED_FIELD")] + public Stream [] UnsupportedMember () => []; + """, + "UNSUPPORTED_FIELD", + ""), + "special-xml-array-return" => ( + "", + """ + [return: ExportParameter (ExportParameterKind.XmlPullParser)] + [Export ("unsupported")] + public XmlReader [] UnsupportedMember () => []; + """, + "unsupported", + ""), _ => throw new InvalidOperationException ($"Unknown unsupported [Export] shape '{invalidShape}'."), }; var proj = new XamarinAndroidApplicationProject { @@ -227,6 +277,8 @@ public T UnsupportedMember ( proj.Sources.Add (new BuildItem.Source ("ExportSignatureValidation.cs") { TextContent = () => $$""" using System.Collections.Generic; + using System.IO; + using System.Xml; using Android.Runtime; using Java.Interop; @@ -246,7 +298,13 @@ public SignaturePeer () { using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape}."); + var succeeded = builder.Build (proj); + if (expectedCode == "success") { + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should retain legacy build support for {invalidShape}."); + return; + } + + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject {invalidShape}."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); if (expectedCode == "XA4263") { StringAssertEx.Contains ( diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs index bfd889fd450..726cb3a5dd9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs @@ -26,4 +26,22 @@ public void ScannerAndGenerator_UnsupportedExportSignatureProducesNoMember (stri new JcwJavaSourceGenerator ().Generate (peer, writer); Assert.DoesNotContain (" unsupported (", writer.ToString (), System.StringComparison.Ordinal); } + + [Fact] + public void ScannerAndGenerator_IgnoreExportAttributeLookalikes () + { + var peer = FindFixtureByJavaName ("my/app/ExportAttributeLookalikes"); + + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "LookalikeExport"); + Assert.Contains (peer.MarshalMethods, method => + method.ManagedMethodName == "RealExport" && + method.JniName == "realExport" && + method.JniSignature == "(Ljava/lang/String;)Ljava/lang/String;"); + + using var writer = new StringWriter (); + new JcwJavaSourceGenerator ().Generate (peer, writer); + var java = writer.ToString (); + Assert.DoesNotContain ("NOT_AN_EXPORT", java, System.StringComparison.Ordinal); + Assert.Contains ("realExport (java.lang.String", java, System.StringComparison.Ordinal); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 88ca5b2f78b..2baf133e290 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -10,6 +10,27 @@ public enum JniHandleOwnership } } +namespace Lookalike +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportAttribute : Attribute + { + public ExportAttribute (string name) { } + } + + [AttributeUsage (AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false)] + public sealed class ExportParameterAttribute : Attribute + { + public ExportParameterAttribute (Java.Interop.ExportParameterKind kind) { } + } + + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportFieldAttribute : Attribute + { + public ExportFieldAttribute (string name) { } + } +} + namespace Java.Interop { public struct JniObjectReference diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index d749680077b..85ce4d8fbed 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -652,6 +652,21 @@ public class GenericExportType : Java.Lang.Object public int UnsupportedMember () => 0; } + [Register ("my/app/ExportAttributeLookalikes")] + public class ExportAttributeLookalikes : Java.Lang.Object + { + [Lookalike.Export ("NOT_AN_EXPORT")] + public void LookalikeExport ( + [Lookalike.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedExportValue value) + { + } + + [Java.Interop.Export ("realExport")] + public string RealExport ( + [Lookalike.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] string value) + => value; + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods. From 1b9e59df0e8b0b316135facff37381a3ffd97535 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 13:17:43 +0200 Subject: [PATCH 11/45] [typemap] Validate exported constructors Ignore static exported constructors, diagnose unresolved instance constructor parameters without overlapping XA4260 shape ownership, preserve ExportParameter kinds through UCO activation, and resolve enum signatures by assembly identity. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 4 + .../Generator/Model/TypeMapAssemblyData.cs | 5 + .../Generator/ModelBuilder.cs | 1 + .../Generator/TypeMapAssemblyEmitter.cs | 59 +++-------- .../Scanner/JavaPeerInfo.cs | 5 + .../Scanner/JavaPeerScanner.cs | 99 ++++++++++++++----- .../TrimmableTypeMapBuildTests.cs | 59 ++++++++++- .../ScannerExportShapesTests.cs | 3 + .../UserTypesFixture/UserTypes.cs | 10 ++ .../Generator/ExportSignatureTests.cs | 63 ++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 10 +- .../TypeMapAssemblyGeneratorTests.cs | 30 ++++++ .../Generator/TypeMapModelBuilderTests.cs | 15 +++ .../TestFixtures/TestTypes.cs | 34 +++++++ 14 files changed, 324 insertions(+), 73 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 185c2637dcf..fdaca8c7f46 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -118,6 +118,10 @@ static void WriteUcoConstructor (this BinaryWriter writer, UcoConstructorData co foreach (var parameterType in constructor.ManagedParameterTypes) { writer.WriteTypeRef (parameterType); } + writer.Write (constructor.ParameterKinds.Count); + foreach (var parameterKind in constructor.ParameterKinds) { + writer.Write ((int) parameterKind); + } } static void WriteNativeRegistration (this BinaryWriter writer, NativeRegistrationData registration) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 054a44f1795..03100fa3cae 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -407,6 +407,11 @@ sealed record UcoConstructorData /// to the corresponding managed type before calling the user ctor. /// public IReadOnlyList ManagedParameterTypes { get; init; } = []; + + /// + /// Per-parameter [ExportParameter] kinds for constructor argument marshalling. + /// + public IReadOnlyList ParameterKinds { get; init; } = []; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 82b1dd2d288..ff789c98788 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -386,6 +386,7 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) AssemblyName = peer.AssemblyName, }, ManagedParameterTypes = ctor.ManagedParameterTypes, + ParameterKinds = ctor.ManagedParameterExportKinds, HasMatchingManagedCtor = ctor.HasMatchingManagedCtor, }); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 56a160a7914..4633e1df0b5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1182,7 +1182,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy if (uco.HasMatchingManagedCtor) { var ctorRef = AddManagedCtorRef (targetTypeRef, uco.ManagedParameterTypes); var managedCtorHandle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, - enc => EmitManagedConstructorActivation (enc, targetTypeRef, ctorRef, uco.ManagedParameterTypes, jniParams), + enc => EmitManagedConstructorActivation (enc, targetTypeRef, ctorRef, uco.ManagedParameterTypes, uco.ParameterKinds, jniParams), blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); AddUnmanagedCallersOnlyAttribute (managedCtorHandle); return managedCtorHandle; @@ -1278,6 +1278,7 @@ void EmitManagedConstructorActivation ( EntityHandle targetTypeRef, MemberReferenceHandle ctorRef, IReadOnlyList managedParameterTypes, + IReadOnlyList parameterKinds, IReadOnlyList jniParams) { var havePeer = enc.DefineLabel (); @@ -1303,7 +1304,8 @@ void EmitManagedConstructorActivation ( enc.MarkLabel (havePeer); enc.LoadLocal (4); for (int i = 0; i < managedParameterTypes.Count; i++) { - EmitManagedConstructorArgument (enc, managedParameterTypes [i], jniParams [i], i + 2); + var parameterKind = i < parameterKinds.Count ? parameterKinds [i] : ExportParameterKindInfo.Unspecified; + EmitManagedConstructorArgument (enc, managedParameterTypes [i], parameterKind, jniParams [i], i + 2); } enc.Call (ctorRef, managedParameterTypes.Count, isInstance: true); @@ -1383,53 +1385,14 @@ void EmitUcoConstructorBodyWithMarshal (TrackedInstructionEncoder encoder, Contr cfb.AddFinallyRegion (tryStart, finallyStart, finallyStart, afterAll); } - void EmitManagedConstructorArgument (TrackedInstructionEncoder encoder, TypeRefData managedType, JniParamKind jniKind, int argumentIndex) + void EmitManagedConstructorArgument ( + TrackedInstructionEncoder encoder, + TypeRefData managedType, + ExportParameterKindInfo exportKind, + JniParamKind jniKind, + int argumentIndex) { - if (managedType.ManagedTypeName == "System.Boolean") { - encoder.LoadArgument (argumentIndex); - encoder.LoadConstantI4 (0); - encoder.OpCode (ILOpCode.Cgt_un); - return; - } - - if (jniKind != JniParamKind.Object) { - encoder.LoadArgument (argumentIndex); - return; - } - - if (managedType.ManagedTypeName == "System.String") { - encoder.LoadArgument (argumentIndex); - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - encoder.Call (_jniEnvGetStringRef, parameterCount: 2, returnsValue: true); - return; - } - - if (TryGetSzArrayElementType (managedType.ManagedTypeName, out var elementType)) { - var arrayType = ResolveManagedTypeHandle (managedType.ManagedTypeName, managedType.AssemblyName); - var elementTypeHandle = ResolveManagedTypeHandle (elementType, managedType.AssemblyName); - - encoder.LoadArgument (argumentIndex); - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - encoder.LoadToken (elementTypeHandle); - encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - encoder.Call (_jniEnvGetArrayRef, parameterCount: 3, returnsValue: true); - encoder.CastClass (arrayType); - return; - } - - encoder.LoadArgument (argumentIndex); - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - if (managedType.ManagedTypeName == "System.Object") { - encoder.OpCode (ILOpCode.Ldnull); - encoder.Call (_javaLangObjectGetObjectRef, parameterCount: 3, returnsValue: true); - return; - } - - var managedTypeHandle = ResolveManagedTypeHandle (managedType.ManagedTypeName, managedType.AssemblyName); - encoder.LoadToken (managedTypeHandle); - encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - encoder.Call (_javaLangObjectGetObjectRef, parameterCount: 3, returnsValue: true); - encoder.CastClass (managedTypeHandle); + GetExportMethodDispatchEmitter ().LoadManagedArgument (encoder, managedType, exportKind, jniKind, argumentIndex); } EntityHandle ResolveRuntimeTypeSpec (RuntimeTypeSpec type) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 86915bff704..114856936f9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -368,6 +368,11 @@ public sealed record JavaConstructorInfo /// internal IReadOnlyList ManagedParameterTypes { get; init; } = []; + /// + /// Per-parameter [ExportParameter] kinds for constructor argument marshalling. + /// + internal IReadOnlyList ManagedParameterExportKinds { get; init; } = []; + /// /// True when this Java constructor has a matching public managed constructor on the target type. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 037076ea719..40b5915ec57 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -693,6 +693,10 @@ static void SkipArrayShape (ref BlobReader reader) // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); + var methodName = index.Reader.GetString (methodDef.Name); + if (methodName == ".cctor" && (methodDef.Attributes & MethodAttributes.Static) != 0) { + continue; + } if (!ValidateExportField (methodDef, index, isGenericType)) { continue; @@ -824,10 +828,11 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b } var methodName = index.Reader.GetString (methodDef.Name); - if (methodName is ".ctor" or ".cctor") { + bool isConstructor = methodName == ".ctor"; + if (isConstructor && !isExport) { return true; } - if (isExport && isGenericType) { + if (!isConstructor && isExport && isGenericType) { logger?.LogExportOnGenericTypeError (); return false; } @@ -836,14 +841,22 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b var (parameterKinds, returnKind) = GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length); var declaringType = index.Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); string declaringTypeName = MetadataTypeNameResolver.GetFullName (declaringType, index.Reader); - string memberName = $"{declaringTypeName}.{methodName}"; + string memberName = methodName [0] == '.' + ? declaringTypeName + methodName + : $"{declaringTypeName}.{methodName}"; for (int i = 0; i < sig.ParameterTypes.Length; i++) { + if (isConstructor && IsOwnedByConstructorDiagnostics (sig.ParameterTypes [i])) { + continue; + } if (!HasExportSignatureMapping (sig.ParameterTypes [i], parameterKinds [i])) { logger?.LogUnsupportedExportSignatureError (memberName, sig.ParameterTypes [i].DisplayName); return false; } } + if (isConstructor) { + return true; + } if (!HasExportSignatureMapping (sig.ReturnType, returnKind)) { logger?.LogUnsupportedExportSignatureError (memberName, sig.ReturnType.DisplayName); return false; @@ -851,6 +864,22 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b return true; } + /// + /// PR #12567 owns XA4260 for signature shapes that no Java constructor can represent. + /// This layer only adds XA4263 for unresolved types and invalid [ExportParameter] pairs. + /// + internal static bool IsOwnedByConstructorDiagnostics (TypeRefData parameterType) + { + string managedTypeName = parameterType.ManagedTypeName; + return parameterType.GenericArguments.Count > 0 || + managedTypeName.StartsWith ("!", StringComparison.Ordinal) || + managedTypeName == "delegate*" || + managedTypeName.EndsWith ("&", StringComparison.Ordinal) || + managedTypeName.EndsWith ("*", StringComparison.Ordinal) || + (managedTypeName.EndsWith ("]", StringComparison.Ordinal) && + !managedTypeName.EndsWith ("[]", StringComparison.Ordinal)); + } + bool HasExportSignatureMapping (TypeRefData managedType, ExportParameterKindInfo exportKind) { if (TryManagedTypeToJniDescriptor (managedType, exportKind, out _)) { @@ -1319,17 +1348,12 @@ bool IsEnumOrEnumArray (string managedType, string? assemblyName = null) (TypeDefinition typeDef, AssemblyIndex index)? TryFindEnumTypeDefinition (string managedType, string? assemblyName = null) { - // Prefer the typed assembly hint so two assemblies with same-named types - // (one enum, one not) resolve deterministically — assemblyCache - // enumeration order is non-deterministic. - if (assemblyName is { Length: > 0 } && - assemblyCache.TryGetValue (assemblyName, out var hintedIndex) && - hintedIndex.TypesByFullName.TryGetValue (managedType, out var hintedHandle)) { - var hintedDef = hintedIndex.Reader.GetTypeDefinition (hintedHandle); - if (IsEnumType (hintedDef, hintedIndex)) { - return (hintedDef, hintedIndex); + if (assemblyName is { Length: > 0 }) { + if (!TryResolveType (managedType, assemblyName, out var resolvedHandle, out var resolvedIndex)) { + return null; } - // Hinted assembly had a same-named non-enum; keep scanning. + var resolvedType = resolvedIndex.Reader.GetTypeDefinition (resolvedHandle); + return IsEnumType (resolvedType, resolvedIndex) ? (resolvedType, resolvedIndex) : null; } foreach (var index in assemblyCache.Values) { @@ -2724,7 +2748,10 @@ List BuildJavaConstructors (List marshal // Unsupported managed parameter shapes fail in model building for [Export] // constructors; non-[Export] registrations keep the legacy activation fallback. publicConstructors ??= GetSupportedPublicConstructors (typeDef, index); - var managedParams = TryGetMatchingPublicConstructorParameterTypes (publicConstructors, mm.JniSignature); + var managedParams = TryGetMatchingPublicConstructorParameterTypes ( + publicConstructors, + mm.JniSignature, + mm.ManagedParameterExportKinds); ctors.Add (new JavaConstructorInfo { JniSignature = mm.JniSignature, ConstructorIndex = ctorIndex, @@ -2733,6 +2760,7 @@ List BuildJavaConstructors (List marshal HasMatchingManagedCtor = managedParams != null, ManagedParameterTypes = managedParams ?? [], Annotations = mm.Annotations, + ManagedParameterExportKinds = mm.ManagedParameterExportKinds, }); ctorIndex++; } @@ -2753,10 +2781,7 @@ List GetSupportedPublicConstructors (TypeDefinition typeD var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, genericContext: index); bool unsupported = false; foreach (var parameter in sig.ParameterTypes) { - var typeName = parameter.ManagedTypeName; - if (parameter.GenericArguments.Count > 0 || - typeName.EndsWith ("&", StringComparison.Ordinal) || - typeName.EndsWith ("*", StringComparison.Ordinal)) { + if (IsOwnedByConstructorDiagnostics (parameter)) { unsupported = true; break; } @@ -2780,27 +2805,55 @@ List GetSupportedPublicConstructors (TypeDefinition typeD /// parameter types. Returns when no compatible /// constructor exists. /// - static IReadOnlyList? TryGetMatchingPublicConstructorParameterTypes ( + IReadOnlyList? TryGetMatchingPublicConstructorParameterTypes ( List publicConstructors, - string jniSignature) + string jniSignature, + IReadOnlyList parameterKinds) { int closeParen = jniSignature.IndexOf (')'); if (closeParen < 0) { throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'"); } int parameterSignatureLength = closeParen + 1; + var jniParams = parameterKinds.Any (kind => kind != ExportParameterKindInfo.Unspecified) + ? JniSignatureHelper.ParseParameters (jniSignature) + : null; foreach (var constructor in publicConstructors) { - if (constructor.JniParameterSignature.Length != parameterSignatureLength || - string.CompareOrdinal (jniSignature, 0, constructor.JniParameterSignature, 0, parameterSignatureLength) != 0) { + if (jniParams is null) { + if (constructor.JniParameterSignature.Length != parameterSignatureLength || + string.CompareOrdinal (jniSignature, 0, constructor.JniParameterSignature, 0, parameterSignatureLength) != 0) { + continue; + } + } else if (!ManagedConstructorParametersMatchJniSignature (constructor.ParameterTypes, parameterKinds, jniParams)) { continue; } // If multiple overloads with the same JNI-compatible signature exist, match // the first public constructor in metadata order, like TypeManager.Activate. - return [.. constructor.ParameterTypes]; + return constructor.ParameterTypes; } return null; } + bool ManagedConstructorParametersMatchJniSignature ( + IReadOnlyList managedParams, + IReadOnlyList parameterKinds, + IReadOnlyList jniParams) + { + if (managedParams.Count != jniParams.Count) { + return false; + } + + for (int i = 0; i < managedParams.Count; i++) { + var parameterKind = i < parameterKinds.Count ? parameterKinds [i] : ExportParameterKindInfo.Unspecified; + var managedDescriptor = ManagedTypeToJniDescriptor (managedParams [i], parameterKind); + if (!string.Equals (managedDescriptor, jniParams [i].JniType, StringComparison.Ordinal)) { + return false; + } + } + return true; + } + } + /// /// Checks a single method for [ExportField] and adds a JavaFieldInfo if found. /// Called inline during Pass 1 to avoid a separate iteration. diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 835bae51806..4ed24ad6672 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -139,6 +139,18 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-xml-array-return", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-xml-array-return", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-xml-array-return", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-static-constructor", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-static-constructor", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-static-constructor", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-arbitrary", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-arbitrary", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-arbitrary", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-invalid-kind", "XALNS7004")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-invalid-kind", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-invalid-kind", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-valid-kind", "success")] public void Build_ExportSignature_MatchesRuntimeClassification ( string typeMapImplementation, AndroidRuntime runtime, @@ -263,6 +275,48 @@ public void UnsupportedMember ( """, "unsupported", ""), + "export-static-constructor" => ( + "", + """ + [Export] + static SignaturePeer () + { + } + """, + ".cctor", + ""), + "export-constructor-arbitrary" => ( + "public sealed class ManagedOnly { }", + """ + [Export (".ctor", SuperArgumentsString = "")] + public SignaturePeer (ManagedOnly value) + { + } + """, + "SignaturePeer", + ""), + "export-constructor-invalid-kind" => ( + "public sealed class ManagedOnly { }", + """ + [Export (".ctor", SuperArgumentsString = "")] + public SignaturePeer ( + [ExportParameter (ExportParameterKind.InputStream)] ManagedOnly value) + { + } + """, + "SignaturePeer", + ""), + "export-constructor-valid-kind" => ( + "", + """ + [Export (".ctor", SuperArgumentsString = "")] + public SignaturePeer ( + [ExportParameter (ExportParameterKind.InputStream)] Stream value) + { + } + """, + "java.io.InputStream", + ""), _ => throw new InvalidOperationException ($"Unknown unsupported [Export] shape '{invalidShape}'."), }; var proj = new XamarinAndroidApplicationProject { @@ -307,8 +361,11 @@ public SignaturePeer () { Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject {invalidShape}."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); if (expectedCode == "XA4263") { + var expectedMemberName = invalidShape.StartsWith ("export-constructor", StringComparison.Ordinal) + ? "ExportSignatureValidation.SignaturePeer.ctor" + : "ExportSignatureValidation.SignaturePeer.UnsupportedMember"; StringAssertEx.Contains ( - "ExportSignatureValidation.SignaturePeer.UnsupportedMember", + expectedMemberName, builder.LastBuildOutput, "The diagnostic should identify the unsupported managed member." ); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs index 5ccddc99ad0..03bf456b296 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs @@ -123,6 +123,9 @@ public void Export_CrossAssemblySameManagedName_DoesNotBorrowPeerDescriptor () var peer = GetPeer ("ExportAssemblyCollisionShape", javaSourceFixturePath, UserTypesFixturePath); Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedCollision"); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedEnumCollision"); + Assert.DoesNotContain (peer.MarshalMethods, method => method.ManagedMethodName == "UnsupportedEnumField"); + Assert.DoesNotContain (peer.JavaFields, field => field.FieldName == "UNSUPPORTED_ENUM_FIELD"); } // === Phase A: dispatch & declaration shapes === diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index a522c81c155..70aa95a84f0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -408,6 +408,10 @@ namespace UserApp.JavaSourceParity public class SignatureCollision { } + + public class EnumCollision + { + } } namespace UserApp @@ -417,5 +421,11 @@ public class ExportAssemblyCollisionShape : Java.Lang.Object { [Export ("unsupportedCollision")] public void UnsupportedCollision (JavaSourceParity.SignatureCollision value) { } + + [Export ("unsupportedEnumCollision")] + public JavaSourceParity.EnumCollision UnsupportedEnumCollision (JavaSourceParity.EnumCollision value) => value; + + [ExportField ("UNSUPPORTED_ENUM_FIELD")] + public JavaSourceParity.EnumCollision UnsupportedEnumField () => new (); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs index 726cb3a5dd9..aa6479ad2de 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs @@ -44,4 +44,67 @@ public void ScannerAndGenerator_IgnoreExportAttributeLookalikes () Assert.DoesNotContain ("NOT_AN_EXPORT", java, System.StringComparison.Ordinal); Assert.Contains ("realExport (java.lang.String", java, System.StringComparison.Ordinal); } + + [Fact] + public void Scanner_ExportConstructors_ValidateParametersAndPreserveMappings () + { + foreach (var javaName in new [] { + "my/app/ExportConstructorUnsupportedManagedParameter", + "my/app/ExportConstructorInvalidExportParameter", + }) { + var invalidPeer = FindFixtureByJavaName (javaName); + Assert.DoesNotContain (invalidPeer.MarshalMethods, method => method.IsConstructor && method.IsExport); + } + + var mappedPeer = FindFixtureByJavaName ("my/app/ExportConstructorMappedParameter"); + var mappedMethod = Assert.Single (mappedPeer.MarshalMethods, method => method.IsConstructor && method.IsExport); + Assert.Equal ("(Ljava/io/InputStream;)V", mappedMethod.JniSignature); + Assert.Equal ([ExportParameterKindInfo.InputStream], mappedMethod.ManagedParameterExportKinds); + var mappedConstructor = Assert.Single ( + mappedPeer.JavaConstructors, + constructor => constructor.JniSignature == "(Ljava/io/InputStream;)V"); + Assert.True (mappedConstructor.HasMatchingManagedCtor); + Assert.Equal ([ExportParameterKindInfo.InputStream], mappedConstructor.ManagedParameterExportKinds); + + var staticPeer = FindFixtureByJavaName ("my/app/ExportStaticConstructor"); + Assert.DoesNotContain (staticPeer.MarshalMethods, method => method.ManagedMethodName == ".cctor"); + } + + [Theory] + [InlineData ("!0")] + [InlineData ("!!0")] + [InlineData ("System.Int32&")] + [InlineData ("System.Int32*")] + [InlineData ("delegate*")] + [InlineData ("System.String[,]")] + public void ConstructorDiagnostics_OwnUnsupportedSignatureShapes (string managedTypeName) + { + Assert.True (JavaPeerScanner.IsOwnedByConstructorDiagnostics (new TypeRefData { + ManagedTypeName = managedTypeName, + AssemblyName = "Test", + })); + } + + [Fact] + public void ConstructorDiagnostics_OwnGenericInstantiationsOnly () + { + Assert.True (JavaPeerScanner.IsOwnedByConstructorDiagnostics (new TypeRefData { + ManagedTypeName = "System.Collections.Generic.List`1", + AssemblyName = "System.Collections", + GenericArguments = [ + new TypeRefData { + ManagedTypeName = "System.String", + AssemblyName = "System.Runtime", + }, + ], + })); + Assert.False (JavaPeerScanner.IsOwnedByConstructorDiagnostics (new TypeRefData { + ManagedTypeName = "MyApp.ManagedOnly", + AssemblyName = "Test", + })); + Assert.False (JavaPeerScanner.IsOwnedByConstructorDiagnostics (new TypeRefData { + ManagedTypeName = "System.String[]", + AssemblyName = "System.Runtime", + })); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 6a778527638..65e9e8dc358 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -356,7 +356,7 @@ public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPar using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); - Assert.Equal (8, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); + Assert.Equal (10, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4206:", StringComparison.Ordinal))); foreach (var javaName in new [] { "my/app/ExportWithUnsupportedManagedParameter", @@ -376,6 +376,14 @@ public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPar Assert.DoesNotContain (" unsupported (", source.Content, StringComparison.Ordinal); Assert.DoesNotContain ("UNSUPPORTED_FIELD", source.Content, StringComparison.Ordinal); } + + foreach (var javaName in new [] { + "my/app/ExportConstructorUnsupportedManagedParameter", + "my/app/ExportConstructorInvalidExportParameter", + }) { + var peer = result.AllPeers.Single (candidate => candidate.JavaName == javaName); + Assert.DoesNotContain (peer.MarshalMethods, method => method.IsConstructor && method.IsExport); + } } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 48e55b3edc5..b4a51109e48 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1386,6 +1386,36 @@ public void Generate_ExportProxy_SupportsArrayAndLegacyMarshallerHelpers () Assert.Contains ("XmlReaderResourceParser", typeNames); } + [Fact] + public void Generate_ExportConstructor_UsesParameterAdapter () + { + var peer = FindFixtureByJavaName ("my/app/ExportConstructorMappedParameter"); + + using var stream = GenerateAssembly (new [] { peer }, "ExportConstructorMapping"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var adapterMethod = reader.MemberReferences + .Select (handle => (Handle: handle, Reference: reader.GetMemberReference (handle))) + .Single (member => { + if (reader.GetString (member.Reference.Name) != "FromJniHandle" || + member.Reference.Parent.Kind != HandleKind.TypeReference) { + return false; + } + var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Reference.Parent); + return reader.GetString (parent.Name) == "InputStreamInvoker"; + }); + var nctorHandle = reader.MethodDefinitions.Single (handle => + reader.GetString (reader.GetMethodDefinition (handle).Name) == "nctor_0_uco"); + var nctor = reader.GetMethodDefinition (nctorHandle); + var ilBytes = pe.GetMethodBody (nctor.RelativeVirtualAddress).GetILBytes (); + Assert.NotNull (ilBytes); + if (ilBytes is null) { + throw new InvalidOperationException ("Expected exported constructor UCO IL."); + } + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (adapterMethod.Handle))); + } + [Fact] public void Generate_ExportProxy_UsesExactCrossAssemblyTypeReferences () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f66eb72f608..080514ac30c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1570,6 +1570,21 @@ public void Fixture_ExportMarshallingShapes_PropagatesExactManagedTypeMetadata ( Assert.Equal (ExportParameterKindInfo.XmlResourceParser, resourceXmlDispatch.ReturnKind); } + [Fact] + public void Fixture_ExportConstructor_PropagatesParameterKinds () + { + var peer = FindFixtureByJavaName ("my/app/ExportConstructorMappedParameter"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = Assert.Single (model.ProxyTypes); + var constructor = Assert.Single ( + proxy.UcoConstructors, + candidate => candidate.JniSignature == "(Ljava/io/InputStream;)V"); + + Assert.True (constructor.HasMatchingManagedCtor); + Assert.Equal ("System.IO.Stream", constructor.ManagedParameterTypes [0].ManagedTypeName); + Assert.Equal ([ExportParameterKindInfo.InputStream], constructor.ParameterKinds); + } + [Fact] public void Fixture_CustomView_HasTwoConstructorWrappers () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 85ce4d8fbed..61f409b994f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -667,6 +667,40 @@ public string RealExport ( => value; } + [Register ("my/app/ExportConstructorUnsupportedManagedParameter")] + public class ExportConstructorUnsupportedManagedParameter : Android.App.Activity + { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportConstructorUnsupportedManagedParameter (UnsupportedExportValue value) { } + } + + [Register ("my/app/ExportConstructorInvalidExportParameter")] + public class ExportConstructorInvalidExportParameter : Android.App.Activity + { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportConstructorInvalidExportParameter ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedExportValue value) + { + } + } + + [Register ("my/app/ExportConstructorMappedParameter")] + public class ExportConstructorMappedParameter : Android.App.Activity + { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportConstructorMappedParameter ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] Stream value) + { + } + } + + [Register ("my/app/ExportStaticConstructor")] + public class ExportStaticConstructor : Java.Lang.Object + { + [Java.Interop.Export] + static ExportStaticConstructor () { } + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods. From 682a6dffdd38133eef104b177b1d990c7fa6a423 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:10:41 +0200 Subject: [PATCH 12/45] [typemap] Preserve exported constructor identity Ignore Export name overrides on metadata constructors, assert UCO dispatch calls the managed constructor rather than a virtual method, and recursively reserve nested structural parameter shapes for XA4260. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 11 ++++++- .../TrimmableTypeMapBuildTests.cs | 29 ++++++++++++++++++- .../Generator/ExportSignatureTests.cs | 5 ++++ .../Generator/FixtureTestBase.cs | 5 ++++ .../TypeMapAssemblyGeneratorTests.cs | 26 +++++++++++++---- .../TestFixtures/TestTypes.cs | 4 +-- 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 40b5915ec57..1c26036d146 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -871,6 +871,11 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b internal static bool IsOwnedByConstructorDiagnostics (TypeRefData parameterType) { string managedTypeName = parameterType.ManagedTypeName; + if (managedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + return IsOwnedByConstructorDiagnostics (parameterType with { + ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 2), + }); + } return parameterType.GenericArguments.Count > 0 || managedTypeName.StartsWith ("!", StringComparison.Ordinal) || managedTypeName == "delegate*" || @@ -1927,6 +1932,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = index.DecodeAttribute (ca); + var managedName = index.Reader.GetString (methodDef.Name); // [Export("name")] or [Export] (uses method name) string? exportName = null; @@ -1969,7 +1975,10 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, } if (string.IsNullOrEmpty (exportName)) { - exportName = index.Reader.GetString (methodDef.Name); + exportName = managedName; + } + if (managedName == ".ctor") { + exportName = ".ctor"; } string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 4ed24ad6672..532989863f0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -151,6 +151,12 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-valid-kind", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-named-constructor-arbitrary", "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-named-constructor-arbitrary", "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-named-constructor-arbitrary", "XA4263")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-named-constructor-valid-kind", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-named-constructor-valid-kind", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-named-constructor-valid-kind", "success")] public void Build_ExportSignature_MatchesRuntimeClassification ( string typeMapImplementation, AndroidRuntime runtime, @@ -317,6 +323,27 @@ public SignaturePeer ( """, "java.io.InputStream", ""), + "export-named-constructor-arbitrary" => ( + "public sealed class ManagedOnly { }", + """ + [Export ("notAConstructor", SuperArgumentsString = "")] + public SignaturePeer (ManagedOnly value) + { + } + """, + "SignaturePeer", + ""), + "export-named-constructor-valid-kind" => ( + "", + """ + [Export ("notAConstructor", SuperArgumentsString = "")] + public SignaturePeer ( + [ExportParameter (ExportParameterKind.InputStream)] Stream value) + { + } + """, + "java.io.InputStream", + ""), _ => throw new InvalidOperationException ($"Unknown unsupported [Export] shape '{invalidShape}'."), }; var proj = new XamarinAndroidApplicationProject { @@ -361,7 +388,7 @@ public SignaturePeer () { Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject {invalidShape}."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); if (expectedCode == "XA4263") { - var expectedMemberName = invalidShape.StartsWith ("export-constructor", StringComparison.Ordinal) + var expectedMemberName = invalidShape.Contains ("constructor", StringComparison.Ordinal) ? "ExportSignatureValidation.SignaturePeer.ctor" : "ExportSignatureValidation.SignaturePeer.UnsupportedMember"; StringAssertEx.Contains ( diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs index aa6479ad2de..9945dbd49e4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportSignatureTests.cs @@ -58,7 +58,9 @@ public void Scanner_ExportConstructors_ValidateParametersAndPreserveMappings () var mappedPeer = FindFixtureByJavaName ("my/app/ExportConstructorMappedParameter"); var mappedMethod = Assert.Single (mappedPeer.MarshalMethods, method => method.IsConstructor && method.IsExport); + Assert.Equal (".ctor", mappedMethod.JniName); Assert.Equal ("(Ljava/io/InputStream;)V", mappedMethod.JniSignature); + Assert.DoesNotContain (mappedPeer.MarshalMethods, method => method.JniName == "notAConstructor"); Assert.Equal ([ExportParameterKindInfo.InputStream], mappedMethod.ManagedParameterExportKinds); var mappedConstructor = Assert.Single ( mappedPeer.JavaConstructors, @@ -77,6 +79,9 @@ public void Scanner_ExportConstructors_ValidateParametersAndPreserveMappings () [InlineData ("System.Int32*")] [InlineData ("delegate*")] [InlineData ("System.String[,]")] + [InlineData ("System.String[,][]")] + [InlineData ("System.Int32*[]")] + [InlineData ("delegate*[]")] public void ConstructorDiagnostics_OwnUnsupportedSignatureShapes (string managedTypeName) { Assert.True (JavaPeerScanner.IsOwnedByConstructorDiagnostics (new TypeRefData { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 9f4c2460155..dd12967da2f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -190,6 +190,11 @@ private protected static bool ILContainsCallToken (byte[] ilBytes, int token) return ILContainsOpcodeToken (ilBytes, token, (byte) ILOpCode.Call, (byte) ILOpCode.Callvirt); } + private protected static bool ILContainsCallvirtToken (byte[] ilBytes, int token) + { + return ILContainsOpcodeToken (ilBytes, token, (byte) ILOpCode.Callvirt); + } + private protected static bool ILContainsNewobjToken (byte[] ilBytes, int token) { return ILContainsOpcodeToken (ilBytes, token, (byte) ILOpCode.Newobj); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index b4a51109e48..5a5a0c2f4f8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1405,15 +1405,29 @@ public void Generate_ExportConstructor_UsesParameterAdapter () var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Reference.Parent); return reader.GetString (parent.Name) == "InputStreamInvoker"; }); - var nctorHandle = reader.MethodDefinitions.Single (handle => - reader.GetString (reader.GetMethodDefinition (handle).Name) == "nctor_0_uco"); - var nctor = reader.GetMethodDefinition (nctorHandle); - var ilBytes = pe.GetMethodBody (nctor.RelativeVirtualAddress).GetILBytes (); + var managedConstructor = reader.MemberReferences + .Select (handle => (Handle: handle, Reference: reader.GetMemberReference (handle))) + .Single (member => { + if (reader.GetString (member.Reference.Name) != ".ctor" || + member.Reference.Parent.Kind != HandleKind.TypeReference) { + return false; + } + var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Reference.Parent); + return reader.GetString (parent.Name) == "ExportConstructorMappedParameter"; + }); + var ilBytes = reader.MethodDefinitions + .Select (handle => reader.GetMethodDefinition (handle)) + .Where (method => reader.GetString (method.Name).StartsWith ("nctor_", StringComparison.Ordinal)) + .Select (method => pe.GetMethodBody (method.RelativeVirtualAddress).GetILBytes ()) + .FirstOrDefault (bytes => bytes is not null && + ILContainsCallToken (bytes, MetadataTokens.GetToken (managedConstructor.Handle))); Assert.NotNull (ilBytes); - if (ilBytes is null) { + if (ilBytes is null) throw new InvalidOperationException ("Expected exported constructor UCO IL."); - } Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (adapterMethod.Handle))); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (managedConstructor.Handle))); + Assert.False (ILContainsCallvirtToken (ilBytes, MetadataTokens.GetToken (managedConstructor.Handle))); + Assert.False (ILContainsNewobjToken (ilBytes, MetadataTokens.GetToken (managedConstructor.Handle))); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 61f409b994f..a27e7464572 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -670,7 +670,7 @@ public string RealExport ( [Register ("my/app/ExportConstructorUnsupportedManagedParameter")] public class ExportConstructorUnsupportedManagedParameter : Android.App.Activity { - [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + [Java.Interop.Export ("notAConstructor", SuperArgumentsString = "")] public ExportConstructorUnsupportedManagedParameter (UnsupportedExportValue value) { } } @@ -687,7 +687,7 @@ public ExportConstructorInvalidExportParameter ( [Register ("my/app/ExportConstructorMappedParameter")] public class ExportConstructorMappedParameter : Android.App.Activity { - [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + [Java.Interop.Export ("notAConstructor", SuperArgumentsString = "")] public ExportConstructorMappedParameter ( [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] Stream value) { From 9afcfeffcf1e53ecb1540f3ffc74f2e6e623c01c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 15:32:29 +0200 Subject: [PATCH 13/45] [typemap] Validate special mapping type identity Require Stream and XmlReader special export mappings to resolve from their framework assemblies or forwarders, preventing same-full-name user types from receiving framework adapters. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/xa4263.md | 1 + .../Scanner/JavaPeerScanner.cs | 30 ++++++-- .../TrimmableTypeMapBuildTests.cs | 73 +++++++++++++++++++ ...k.TrimmableTypeMap.IntegrationTests.csproj | 3 + .../ScannerExportShapesTests.cs | 14 ++++ .../SpecialTypeCollisionFixture.csproj | 48 ++++++++++++ .../SpecialTypeCollisionTypes.cs | 39 ++++++++++ .../Scanner/JavaPeerScannerTests.Behavior.cs | 3 + 8 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/SpecialTypeCollisionFixture/SpecialTypeCollisionFixture.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/SpecialTypeCollisionFixture/SpecialTypeCollisionTypes.cs diff --git a/Documentation/docs-mobile/messages/xa4263.md b/Documentation/docs-mobile/messages/xa4263.md index c672ebf7228..fa454a2957a 100644 --- a/Documentation/docs-mobile/messages/xa4263.md +++ b/Documentation/docs-mobile/messages/xa4263.md @@ -25,3 +25,4 @@ This error also occurs when `[ExportParameter]` specifies a Java mapping that is Use a primitive, string, array, Java peer, or another supported managed-to-Java type. Apply input/output stream mappings only to `Stream`, and XML parser mappings only to `XmlReader`. Do not apply these scalar mappings to arrays or other wrappers around those types. +The types must be the framework `System.IO.Stream` and `System.Xml.XmlReader` types, not user-defined types with the same full name. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 1c26036d146..e629d8e202b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2124,7 +2124,7 @@ bool TryManagedTypeToJniDescriptor ( out string descriptor) { if (exportKind != ExportParameterKindInfo.Unspecified) { - return TryGetExportParameterDescriptor (managedType.ManagedTypeName, exportKind, out descriptor); + return TryGetExportParameterDescriptor (managedType, exportKind, out descriptor); } if (managedType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { @@ -2181,21 +2181,37 @@ bool TryManagedTypeToJniDescriptor ( return false; } - static bool TryGetExportParameterDescriptor ( - string managedTypeName, + bool TryGetExportParameterDescriptor ( + TypeRefData managedType, ExportParameterKindInfo exportKind, out string descriptor) { descriptor = exportKind switch { - ExportParameterKindInfo.InputStream when managedTypeName == "System.IO.Stream" => "Ljava/io/InputStream;", - ExportParameterKindInfo.OutputStream when managedTypeName == "System.IO.Stream" => "Ljava/io/OutputStream;", - ExportParameterKindInfo.XmlPullParser when managedTypeName == "System.Xml.XmlReader" => "Lorg/xmlpull/v1/XmlPullParser;", - ExportParameterKindInfo.XmlResourceParser when managedTypeName == "System.Xml.XmlReader" => "Landroid/content/res/XmlResourceParser;", + ExportParameterKindInfo.InputStream when IsSpecialManagedType (managedType, "System.IO.Stream", "System.Runtime", "System.Private.CoreLib") => + "Ljava/io/InputStream;", + ExportParameterKindInfo.OutputStream when IsSpecialManagedType (managedType, "System.IO.Stream", "System.Runtime", "System.Private.CoreLib") => + "Ljava/io/OutputStream;", + ExportParameterKindInfo.XmlPullParser when IsSpecialManagedType (managedType, "System.Xml.XmlReader", "System.Xml.ReaderWriter") => + "Lorg/xmlpull/v1/XmlPullParser;", + ExportParameterKindInfo.XmlResourceParser when IsSpecialManagedType (managedType, "System.Xml.XmlReader", "System.Xml.ReaderWriter") => + "Landroid/content/res/XmlResourceParser;", _ => "", }; return descriptor.Length > 0; } + bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, params string [] assemblyNames) + { + if (!string.Equals (managedType.ManagedTypeName, managedTypeName, StringComparison.Ordinal)) { + return false; + } + if (assemblyNames.Contains (managedType.AssemblyName, StringComparer.Ordinal)) { + return true; + } + return TryResolveType (managedTypeName, managedType.AssemblyName, out _, out var resolvedIndex) && + assemblyNames.Contains (resolvedIndex.MetadataAssemblyName, StringComparer.Ordinal); + } + /// /// Returns the JNI descriptor for primitive types and System.String. /// Returns null for all other types. diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 532989863f0..1f081f4a397 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -400,6 +400,79 @@ public SignaturePeer () { AssertNoExportOutputs (builder, marker); } + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void Build_SpecialMappingLookalikeTypes_ReportXA4263WithoutPartialOutputs (AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.Sources.Add (new BuildItem.Source ("SpecialMappingLookalikes.cs") { + TextContent = () => """ + using Android.Runtime; + using Java.Interop; + + namespace System.IO { + public class Stream { + } + } + + namespace System.Xml { + public class XmlReader { + } + } + + namespace SpecialMappingLookalikes { + [Register ("com/example/exports/SpecialMappingLookalikePeer")] + public class SpecialMappingLookalikePeer : Java.Lang.Object { + [return: ExportParameter (ExportParameterKind.OutputStream)] + [Export ("invalidStream")] + public System.IO.Stream InvalidStream ( + [ExportParameter (ExportParameterKind.InputStream)] System.IO.Stream value) + => value; + + [return: ExportParameter (ExportParameterKind.XmlPullParser)] + [ExportField ("INVALID_XML_FIELD")] + public System.Xml.XmlReader InvalidXmlField () => new (); + + [Export ("notAConstructor", SuperArgumentsString = "")] + public SpecialMappingLookalikePeer ( + [ExportParameter (ExportParameterKind.InputStream)] System.IO.Stream value) + { + } + } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/trimmable should reject special-mapping lookalike types."); + foreach (var memberName in new [] { + "SpecialMappingLookalikes.SpecialMappingLookalikePeer.InvalidStream", + "SpecialMappingLookalikes.SpecialMappingLookalikePeer.InvalidXmlField", + "SpecialMappingLookalikes.SpecialMappingLookalikePeer.ctor", + }) { + Assert.IsTrue ( + builder.LastBuildOutput.Any (line => + line.Contains ("error XA4263", StringComparison.Ordinal) && + line.Contains (memberName, StringComparison.Ordinal)), + $"The build should report XA4263 for '{memberName}'." + ); + } + AssertNoExportOutputs (builder, "invalidStream"); + } + static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( AndroidRuntime runtime, string typeMapImplementation, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj index 96df7a6693b..7e5b7d0bd14 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj @@ -22,6 +22,7 @@ + @@ -36,6 +37,8 @@ ReferenceOutputAssembly=false because we don't compile against it — we just need the DLL on disk. --> + false + + false + @@ -38,6 +42,7 @@ <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" /> + <_TestFixtureFiles Include="InvalidConstructorFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\InvalidConstructorFixtures.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 3e051ecffc7..c43f12c95fa 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -1,4 +1,8 @@ +using System; +using System.IO; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -155,6 +159,76 @@ public void UnsignedPrimitiveCtor_MapsCorrectly () Assert.Contains ("(SIJ)V", ctorSigs); } + [Theory] + [InlineData ("MyApp.GenericParameterCtorActivity`1", "!0")] + [InlineData ("MyApp.GenericInstantiationCtorActivity", "System.Collections.Generic.List`1")] + [InlineData ("MyApp.ByRefCtorActivity", "System.Int32&")] + [InlineData ("MyApp.PointerCtorActivity", "System.Int32*")] + [InlineData ("MyApp.FunctionPointerCtorActivity", "delegate*")] + [InlineData ("MyApp.RectangularArrayCtorActivity", "System.String[,]")] + public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, string parameterType) + { + var diagnostics = ScanConstructorDiagnostics (managedTypeName); + var diagnostic = Assert.Single (diagnostics); + Assert.Equal (ConstructorDiagnosticKind.UnsupportedParameterType, diagnostic.Kind); + Assert.Equal (parameterType, diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.SignedUnsignedCollisionActivity", "(I)V")] + [InlineData ("MyApp.AliasedTypeCollisionActivity", "(Lmy/app/AliasOne;)V")] + public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, string jniSignature) + { + var diagnostics = ScanConstructorDiagnostics (managedTypeName); + var diagnostic = Assert.Single (diagnostics); + Assert.Equal (ConstructorDiagnosticKind.AmbiguousJniSignature, diagnostic.Kind); + Assert.Equal (jniSignature, diagnostic.Detail); + } + + [Fact] + public void MissingBaseConstructor_IsDiagnosed () + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.MissingBaseCtorActivity")); + Assert.Equal (ConstructorDiagnosticKind.MissingBaseConstructor, diagnostic.Kind); + Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); + } + + [Fact] + public void InvalidSuperArgumentsString_IsDiagnosed () + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.InvalidSuperArgumentsActivity")); + Assert.Equal (ConstructorDiagnosticKind.InvalidSuperArgumentsString, diagnostic.Kind); + Assert.Equal ("p1", diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.EnumCtorActivity")] + [InlineData ("MyApp.ExplicitPointerCtorActivity")] + [InlineData ("MyApp.BindingPointerCtor")] + [InlineData ("MyApp.ValidSuperExpressionActivity")] + public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) + { + Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); + } + + static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) + { + using var scanner = new JavaPeerScanner (); + var testAssemblyDir = Path.GetDirectoryName (typeof (ConstructorDetectionTests).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory."); + using var fixtureReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var invalidFixturePath = Path.Combine (testAssemblyDir, "InvalidConstructorFixtures.dll"); + using var invalidFixtureReader = new PEReader (File.OpenRead (invalidFixturePath)); + var fixtureMetadata = fixtureReader.GetMetadataReader (); + var invalidFixtureMetadata = invalidFixtureReader.GetMetadataReader (); + var assemblies = new [] { + (fixtureMetadata.GetString (fixtureMetadata.GetAssemblyDefinition ().Name), fixtureReader), + (invalidFixtureMetadata.GetString (invalidFixtureMetadata.GetAssemblyDefinition ().Name), invalidFixtureReader), + }; + var peer = Assert.Single (scanner.Scan (assemblies), p => p.ManagedTypeName == managedTypeName); + return peer.ConstructorDiagnostics; + } + // --- Regression: HasMatchingManagedCtor semantics --- // These guard the safety net introduced for Java.Lang.Thread+RunnableImplementor: // when a Java ctor (e.g. ()V seeded from a [Register]'d base) has no matching From 94a29532865d0813ea1e73a6d0476cf404157dc9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 12:56:04 +0200 Subject: [PATCH 17/45] Fix constructor parity test capture Compile the fixture exclusion helper with its required namespaces and record llvm-ir's single-dimensional Java signature for rectangular managed arrays. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ConstructorParityTests.cs | 2 +- .../ScannerComparisonTests.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs index 59bc31846f1..06d874c0517 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs @@ -36,7 +36,7 @@ public void LegacyRectangularArrayConstructor_IsRepresentedAsJavaArray () var fixturePath = UserTypesFixturePath; Assert.NotNull (fixturePath); var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, "UserApp.RectangularArrayConstructor"); - Assert.Single (constructors, c => c.JniSignature == "([[Ljava/lang/String;)V"); + Assert.Single (constructors, c => c.JniSignature == "([Ljava/lang/String;)V"); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 0e53a5f3d00..523dfb37616 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.Metadata; From 173582de32b721da6ac8ec2fef30350766fa235d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 13:00:15 +0200 Subject: [PATCH 18/45] Fix constructor diagnostic edge cases Use effective Export and explicit registration signatures during constructor validation, validate implicit Export base forwarding, detect explicit signature collisions, and reject noncanonical super argument references. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 65 +++++++++++++------ .../InvalidConstructors.cs | 55 ++++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 38 +++++++++-- 3 files changed, 133 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 30081807bf9..2a289d1c7bc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2858,11 +2858,20 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( continue; } - if (TryGetMethodRegisterInfo (methodDef, index, out _, out var exportInfo) && exportInfo is null) { - continue; - } - - if (!TryBuildConstructorJniSignature (signature.ParameterTypes, out var jniSignature, out var unsupportedParameterType)) { + TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo); + string jniSignature; + string unsupportedParameterType; + if (registerInfo?.Signature is string registeredSignature) { + if (exportInfo is not null && + !TryValidateConstructorParameterShapes (signature.ParameterTypes, out unsupportedParameterType)) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.UnsupportedParameterType, + Detail = unsupportedParameterType, + }); + continue; + } + jniSignature = registeredSignature; + } else if (!TryBuildConstructorJniSignature (signature.ParameterTypes, out jniSignature, out unsupportedParameterType)) { diagnostics.Add (new ConstructorDiagnosticInfo { Kind = ConstructorDiagnosticKind.UnsupportedParameterType, Detail = unsupportedParameterType, @@ -2881,12 +2890,14 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( signatures [jniSignature] = managedSignature; } - if (exportInfo is not null) { + if (exportInfo?.SuperArgumentsString is not null) { continue; } - bool hasCompatibleBaseConstructor = baseConstructors.Any (baseCtor => - HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); + bool hasCompatibleBaseConstructor = exportInfo is not null + ? baseConstructors.Any (baseCtor => baseCtor.RegisterInfo.Signature == jniSignature) + : baseConstructors.Any (baseCtor => + HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); if (baseConstructors.Count > 0 && !hasCompatibleBaseConstructor && !hasParameterlessBaseConstructor) { diagnostics.Add (new ConstructorDiagnosticInfo { Kind = ConstructorDiagnosticKind.MissingBaseConstructor, @@ -2912,7 +2923,31 @@ bool TryBuildConstructorJniSignature ( out string jniSignature, out string unsupportedParameterType) { + if (!TryValidateConstructorParameterShapes (parameterTypes, out unsupportedParameterType)) { + jniSignature = ""; + return false; + } + var descriptors = new List (parameterTypes.Count); + foreach (var parameterType in parameterTypes) { + var descriptor = TryGetConstructorJniDescriptor (parameterType); + if (descriptor is null) { + jniSignature = ""; + unsupportedParameterType = parameterType.DisplayName; + return false; + } + descriptors.Add (descriptor); + } + + jniSignature = $"({string.Join ("", descriptors)})V"; + unsupportedParameterType = ""; + return true; + } + + static bool TryValidateConstructorParameterShapes ( + IReadOnlyList parameterTypes, + out string unsupportedParameterType) + { foreach (var parameterType in parameterTypes) { if (parameterType.GenericArguments.Count > 0 || parameterType.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal) || @@ -2921,21 +2956,11 @@ bool TryBuildConstructorJniSignature ( parameterType.ManagedTypeName.EndsWith ("*", StringComparison.Ordinal) || (parameterType.ManagedTypeName.EndsWith ("]", StringComparison.Ordinal) && !parameterType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal))) { - jniSignature = ""; unsupportedParameterType = parameterType.DisplayName; return false; } - - var descriptor = TryGetConstructorJniDescriptor (parameterType); - if (descriptor is null) { - jniSignature = ""; - unsupportedParameterType = parameterType.DisplayName; - return false; - } - descriptors.Add (descriptor); } - jniSignature = $"({string.Join ("", descriptors)})V"; unsupportedParameterType = ""; return true; } @@ -2992,7 +3017,9 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa if (end < superArgumentsString.Length && IsJavaIdentifierCharacter (superArgumentsString [end])) { continue; } - if (!int.TryParse (superArgumentsString.Substring (i + 1, end - i - 1), out var parameterIndex) || + var parameterToken = superArgumentsString.Substring (i + 1, end - i - 1); + if ((parameterToken.Length > 1 && parameterToken [0] == '0') || + !int.TryParse (parameterToken, out var parameterIndex) || parameterIndex >= parameterCount) { return false; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 5e48bf8b2ec..a9ea896672d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -21,6 +21,26 @@ public unsafe class ExplicitPointerCtorActivity : Android.App.Activity public ExplicitPointerCtorActivity (int* value) { } } +[Register ("my/app/ExplicitRegisterCollisionActivity")] +public class ExplicitRegisterCollisionActivity : Android.App.Activity +{ + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCollisionActivity (int value) { } + + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCollisionActivity (uint value) { } +} + +[Register ("my/app/JniSignatureCollisionActivity")] +public class JniSignatureCollisionActivity : Android.App.Activity +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCollisionActivity (int value) { } + + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCollisionActivity (uint value) { } +} + [Register ("my/app/BindingPointerCtor", DoNotGenerateAcw = true)] public unsafe class BindingPointerCtor : Java.Lang.Object { @@ -110,9 +130,44 @@ public class InvalidSuperArgumentsActivity : Android.App.Activity public InvalidSuperArgumentsActivity (string value) { } } +[Register ("my/app/NonCanonicalSuperArgumentZeroActivity")] +public class NonCanonicalSuperArgumentZeroActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p00")] + public NonCanonicalSuperArgumentZeroActivity (string value) { } +} + +[Register ("my/app/NonCanonicalSuperArgumentOneActivity")] +public class NonCanonicalSuperArgumentOneActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p01")] + public NonCanonicalSuperArgumentOneActivity (string first, string second) { } +} + [Register ("my/app/ValidSuperExpressionActivity")] public class ValidSuperExpressionActivity : Android.App.Activity { [Java.Interop.Export (".ctor", SuperArgumentsString = "p0.hashCode()")] public ValidSuperExpressionActivity (string value) { } } + +[Register ("my/app/ExportMappedCtorActivity")] +public class ExportMappedCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor")] + public ExportMappedCtorActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] System.IO.Stream value) { } + + [Java.Interop.Export (".ctor")] + public ExportMappedCtorActivity (Java.Lang.ICharSequence value) { } + + [Java.Interop.Export (".ctor")] + public ExportMappedCtorActivity (System.Collections.IList value) { } +} + +[Register ("my/app/ExportMissingBaseCtorActivity")] +public class ExportMissingBaseCtorActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + public ExportMissingBaseCtorActivity (string value) : base (0) { } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index c43f12c95fa..2a35d96fd5a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -177,6 +177,8 @@ public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, [Theory] [InlineData ("MyApp.SignedUnsignedCollisionActivity", "(I)V")] [InlineData ("MyApp.AliasedTypeCollisionActivity", "(Lmy/app/AliasOne;)V")] + [InlineData ("MyApp.ExplicitRegisterCollisionActivity", "(I)V")] + [InlineData ("MyApp.JniSignatureCollisionActivity", "(I)V")] public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, string jniSignature) { var diagnostics = ScanConstructorDiagnostics (managedTypeName); @@ -194,11 +196,22 @@ public void MissingBaseConstructor_IsDiagnosed () } [Fact] - public void InvalidSuperArgumentsString_IsDiagnosed () + public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed () { - var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.InvalidSuperArgumentsActivity")); + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.ExportMissingBaseCtorActivity")); + Assert.Equal (ConstructorDiagnosticKind.MissingBaseConstructor, diagnostic.Kind); + Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.InvalidSuperArgumentsActivity", "p1")] + [InlineData ("MyApp.NonCanonicalSuperArgumentZeroActivity", "p00")] + [InlineData ("MyApp.NonCanonicalSuperArgumentOneActivity", "p01")] + public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); Assert.Equal (ConstructorDiagnosticKind.InvalidSuperArgumentsString, diagnostic.Kind); - Assert.Equal ("p1", diagnostic.Detail); + Assert.Equal (superArgumentsString, diagnostic.Detail); } [Theory] @@ -211,7 +224,21 @@ public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTy Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); } - static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) + [Fact] + public void ExportMappedConstructors_UseEffectiveJniSignaturesWithoutDiagnostics () + { + var peer = ScanPeer ("MyApp.ExportMappedCtorActivity"); + Assert.Empty (peer.ConstructorDiagnostics); + var signatures = peer.JavaConstructors.Select (c => c.JniSignature).ToList (); + Assert.Contains ("(Ljava/io/InputStream;)V", signatures); + Assert.Contains ("(Ljava/lang/CharSequence;)V", signatures); + Assert.Contains ("(Ljava/util/List;)V", signatures); + } + + static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) => + ScanPeer (managedTypeName).ConstructorDiagnostics; + + static JavaPeerInfo ScanPeer (string managedTypeName) { using var scanner = new JavaPeerScanner (); var testAssemblyDir = Path.GetDirectoryName (typeof (ConstructorDetectionTests).Assembly.Location) @@ -225,8 +252,7 @@ static System.Collections.Generic.IReadOnlyList ScanC (fixtureMetadata.GetString (fixtureMetadata.GetAssemblyDefinition ().Name), fixtureReader), (invalidFixtureMetadata.GetString (invalidFixtureMetadata.GetAssemblyDefinition ().Name), invalidFixtureReader), }; - var peer = Assert.Single (scanner.Scan (assemblies), p => p.ManagedTypeName == managedTypeName); - return peer.ConstructorDiagnostics; + return Assert.Single (scanner.Scan (assemblies), p => p.ManagedTypeName == managedTypeName); } // --- Regression: HasMatchingManagedCtor semantics --- From 4f5f7be82238e9cf868987efb5fc8b0853c2b327 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 13:11:15 +0200 Subject: [PATCH 19/45] Harden constructor diagnostic analysis Validate explicit constructor base calls by declared JNI signature, analyze non-public registered constructors, distinguish inferred parameterless fallbacks, and parse super argument references outside Java literals, comments, and qualified member access. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 78 ++++++++++++++-- .../InvalidConstructors.cs | 88 ++++++++++++++++--- .../Scanner/ConstructorDetectionTests.cs | 14 ++- 3 files changed, 159 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 2a289d1c7bc..c4eeb304055 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2848,17 +2848,21 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); if ((methodDef.Attributes & MethodAttributes.Static) != 0 || - (methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public || index.Reader.GetString (methodDef.Name) != ".ctor") { continue; } + bool hasExplicitRegistration = TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo); + if ((methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public && + !hasExplicitRegistration) { + continue; + } + var signature = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); if (IsActivationConstructor (signature.ParameterTypes)) { continue; } - TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo); string jniSignature; string unsupportedParameterType; if (registerInfo?.Signature is string registeredSignature) { @@ -2890,15 +2894,18 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( signatures [jniSignature] = managedSignature; } - if (exportInfo?.SuperArgumentsString is not null) { + if (exportInfo?.SuperArgumentsString is string superArgumentsString && + superArgumentsString.Length > 0) { continue; } - bool hasCompatibleBaseConstructor = exportInfo is not null - ? baseConstructors.Any (baseCtor => baseCtor.RegisterInfo.Signature == jniSignature) + string forwardedBaseSignature = exportInfo?.SuperArgumentsString == "" ? "()V" : jniSignature; + bool hasCompatibleBaseConstructor = hasExplicitRegistration + ? baseConstructors.Any (baseCtor => baseCtor.RegisterInfo.Signature == forwardedBaseSignature) : baseConstructors.Any (baseCtor => HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); - if (baseConstructors.Count > 0 && !hasCompatibleBaseConstructor && !hasParameterlessBaseConstructor) { + bool canUseParameterlessBaseConstructor = !hasExplicitRegistration && hasParameterlessBaseConstructor; + if (baseConstructors.Count > 0 && !hasCompatibleBaseConstructor && !canUseParameterlessBaseConstructor) { diagnostics.Add (new ConstructorDiagnosticInfo { Kind = ConstructorDiagnosticKind.MissingBaseConstructor, Detail = jniSignature, @@ -3003,10 +3010,66 @@ static bool IsActivationConstructor (IReadOnlyList parameterTypes) static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) { + bool inString = false; + bool inCharacter = false; + bool inLineComment = false; + bool inBlockComment = false; + bool escaped = false; + char previousSignificantCharacter = '\0'; + for (int i = 0; i < superArgumentsString.Length; i++) { + char current = superArgumentsString [i]; + char next = i + 1 < superArgumentsString.Length ? superArgumentsString [i + 1] : '\0'; + + if (inLineComment) { + if (current == '\r' || current == '\n') { + inLineComment = false; + } + continue; + } + if (inBlockComment) { + if (current == '*' && next == '/') { + inBlockComment = false; + i++; + } + continue; + } + if (inString || inCharacter) { + if (escaped) { + escaped = false; + } else if (current == '\\') { + escaped = true; + } else if ((inString && current == '"') || (inCharacter && current == '\'')) { + inString = false; + inCharacter = false; + previousSignificantCharacter = current; + } + continue; + } + if (current == '/' && next == '/') { + inLineComment = true; + i++; + continue; + } + if (current == '/' && next == '*') { + inBlockComment = true; + i++; + continue; + } + if (current == '"' || current == '\'') { + inString = current == '"'; + inCharacter = current == '\''; + escaped = false; + continue; + } + if (superArgumentsString [i] != 'p' || i + 1 >= superArgumentsString.Length || !char.IsDigit (superArgumentsString [i + 1]) || - (i > 0 && IsJavaIdentifierCharacter (superArgumentsString [i - 1]))) { + (i > 0 && IsJavaIdentifierCharacter (superArgumentsString [i - 1])) || + previousSignificantCharacter == '.') { + if (!char.IsWhiteSpace (current)) { + previousSignificantCharacter = current; + } continue; } @@ -3023,6 +3086,7 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa parameterIndex >= parameterCount) { return false; } + previousSignificantCharacter = superArgumentsString [end - 1]; i = end - 1; } return true; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index a9ea896672d..36c04ef1c2f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -14,31 +14,59 @@ public class EnumCtorActivity : Android.App.Activity public EnumCtorActivity (ConstructorKind value) { } } +[Register ("my/app/IntSignatureBase", DoNotGenerateAcw = true)] +public class IntSignatureBase : Java.Lang.Object +{ + [Register (".ctor", "(I)V", "")] + public IntSignatureBase (int value) { } +} + +[Register ("my/app/LongSignatureBase", DoNotGenerateAcw = true)] +public class LongSignatureBase : Java.Lang.Object +{ + [Register (".ctor", "(J)V", "")] + public LongSignatureBase (long value) { } +} + [Register ("my/app/ExplicitPointerCtorActivity")] -public unsafe class ExplicitPointerCtorActivity : Android.App.Activity +public unsafe class ExplicitPointerCtorActivity : LongSignatureBase { [Register (".ctor", "(J)V", "")] - public ExplicitPointerCtorActivity (int* value) { } + public ExplicitPointerCtorActivity (int* value) : base ((long)value) { } } [Register ("my/app/ExplicitRegisterCollisionActivity")] -public class ExplicitRegisterCollisionActivity : Android.App.Activity +public class ExplicitRegisterCollisionActivity : IntSignatureBase { [Register (".ctor", "(I)V", "")] - public ExplicitRegisterCollisionActivity (int value) { } + public ExplicitRegisterCollisionActivity (int value) : base (value) { } [Register (".ctor", "(I)V", "")] - public ExplicitRegisterCollisionActivity (uint value) { } + public ExplicitRegisterCollisionActivity (uint value) : base ((int)value) { } } [Register ("my/app/JniSignatureCollisionActivity")] -public class JniSignatureCollisionActivity : Android.App.Activity +public class JniSignatureCollisionActivity : IntSignatureBase { [Java.Interop.JniConstructorSignature ("(I)V")] - public JniSignatureCollisionActivity (int value) { } + public JniSignatureCollisionActivity (int value) : base (value) { } [Java.Interop.JniConstructorSignature ("(I)V")] - public JniSignatureCollisionActivity (uint value) { } + public JniSignatureCollisionActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ExplicitRegisterCompatibleBaseActivity")] +public class ExplicitRegisterCompatibleBaseActivity : IntSignatureBase +{ + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCompatibleBaseActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/JniSignatureCompatibleBaseActivity")] +public class JniSignatureCompatibleBaseActivity : IntSignatureBase +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCompatibleBaseActivity (uint value) : base ((int)value) { } } [Register ("my/app/BindingPointerCtor", DoNotGenerateAcw = true)] @@ -151,17 +179,24 @@ public class ValidSuperExpressionActivity : Android.App.Activity public ValidSuperExpressionActivity (string value) { } } +[Register ("my/app/LexicalSuperArgumentsActivity")] +public class LexicalSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Constants.p1, \"p2\", /* p3 */ p0")] + public LexicalSuperArgumentsActivity (string value) { } +} + [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { - [Java.Interop.Export (".ctor")] + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public ExportMappedCtorActivity ( [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] System.IO.Stream value) { } - [Java.Interop.Export (".ctor")] + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public ExportMappedCtorActivity (Java.Lang.ICharSequence value) { } - [Java.Interop.Export (".ctor")] + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public ExportMappedCtorActivity (System.Collections.IList value) { } } @@ -171,3 +206,34 @@ public class ExportMissingBaseCtorActivity : NoDefaultBase [Java.Interop.Export (".ctor")] public ExportMissingBaseCtorActivity (string value) : base (0) { } } + +[Register ("my/app/ExportEmptySuperMissingBaseActivity")] +public class ExportEmptySuperMissingBaseActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportEmptySuperMissingBaseActivity (string value) : base (0) { } +} + +[Register ("my/app/NonPublicExportCollisionActivity")] +public class NonPublicExportCollisionActivity : IntSignatureBase +{ + [Java.Interop.Export (".ctor")] + protected NonPublicExportCollisionActivity (int value) : base (value) { } + + [Java.Interop.Export (".ctor")] + protected NonPublicExportCollisionActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/NonPublicExportUnsupportedActivity")] +public class NonPublicExportUnsupportedActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + protected NonPublicExportUnsupportedActivity (T value) { } +} + +[Register ("my/app/NonPublicExportMissingBaseActivity")] +public class NonPublicExportMissingBaseActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + protected NonPublicExportMissingBaseActivity (string value) : base (0) { } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 2a35d96fd5a..8a7525eb591 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -166,6 +166,7 @@ public void UnsignedPrimitiveCtor_MapsCorrectly () [InlineData ("MyApp.PointerCtorActivity", "System.Int32*")] [InlineData ("MyApp.FunctionPointerCtorActivity", "delegate*")] [InlineData ("MyApp.RectangularArrayCtorActivity", "System.String[,]")] + [InlineData ("MyApp.NonPublicExportUnsupportedActivity`1", "!0")] public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, string parameterType) { var diagnostics = ScanConstructorDiagnostics (managedTypeName); @@ -179,6 +180,7 @@ public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, [InlineData ("MyApp.AliasedTypeCollisionActivity", "(Lmy/app/AliasOne;)V")] [InlineData ("MyApp.ExplicitRegisterCollisionActivity", "(I)V")] [InlineData ("MyApp.JniSignatureCollisionActivity", "(I)V")] + [InlineData ("MyApp.NonPublicExportCollisionActivity", "(I)V")] public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, string jniSignature) { var diagnostics = ScanConstructorDiagnostics (managedTypeName); @@ -195,10 +197,13 @@ public void MissingBaseConstructor_IsDiagnosed () Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); } - [Fact] - public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed () + [Theory] + [InlineData ("MyApp.ExportMissingBaseCtorActivity")] + [InlineData ("MyApp.ExportEmptySuperMissingBaseActivity")] + [InlineData ("MyApp.NonPublicExportMissingBaseActivity")] + public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed (string managedTypeName) { - var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.ExportMissingBaseCtorActivity")); + var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); Assert.Equal (ConstructorDiagnosticKind.MissingBaseConstructor, diagnostic.Kind); Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); } @@ -217,8 +222,11 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [Theory] [InlineData ("MyApp.EnumCtorActivity")] [InlineData ("MyApp.ExplicitPointerCtorActivity")] + [InlineData ("MyApp.ExplicitRegisterCompatibleBaseActivity")] + [InlineData ("MyApp.JniSignatureCompatibleBaseActivity")] [InlineData ("MyApp.BindingPointerCtor")] [InlineData ("MyApp.ValidSuperExpressionActivity")] + [InlineData ("MyApp.LexicalSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) { Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); From 75ab7da028dce287257f79479282e67abdc6baf9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 14:20:09 +0200 Subject: [PATCH 20/45] Fix constructor comparison test formatting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ScannerComparisonTests.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 523dfb37616..c83316d40bb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -137,35 +137,35 @@ public void ExactMarshalMethods_UserTypesFixture () .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); var newNormalized = newMethods .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); -ExcludeConstructorDiagnosticFixtures (legacyNormalized); -ExcludeConstructorDiagnosticFixtures (newNormalized); + ExcludeConstructorDiagnosticFixtures (legacyNormalized); + ExcludeConstructorDiagnosticFixtures (newNormalized); var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); AssertNoDiffs ("MISSING from new scanner", result.Missing); AssertNoDiffs ("METHOD MISMATCHES", result.MethodMismatches); } -static void ExcludeConstructorDiagnosticFixtures (Dictionary> methods) -{ -var fixtureTypePrefix = "UserApp."; -var fixtureTypeNames = new HashSet (StringComparer.Ordinal) { -$"{fixtureTypePrefix}SignedUnsignedConstructorCollision, UserTypesFixture", -$"{fixtureTypePrefix}AliasedTypeConstructorCollision, UserTypesFixture", -$"{fixtureTypePrefix}GenericParameterConstructor`1, UserTypesFixture", -$"{fixtureTypePrefix}GenericInstantiationConstructor, UserTypesFixture", -$"{fixtureTypePrefix}ByRefConstructor, UserTypesFixture", -$"{fixtureTypePrefix}PointerConstructor, UserTypesFixture", -$"{fixtureTypePrefix}FunctionPointerConstructor, UserTypesFixture", -$"{fixtureTypePrefix}RectangularArrayConstructor, UserTypesFixture", -$"{fixtureTypePrefix}MissingBaseConstructor, UserTypesFixture", -$"{fixtureTypePrefix}InvalidSuperArgumentsConstructor, UserTypesFixture", -}; - -foreach (var javaName in methods.Keys.ToList ()) { -methods [javaName].RemoveAll (group => fixtureTypeNames.Contains (group.ManagedName)); -if (methods [javaName].Count == 0) { -methods.Remove (javaName); -} -} -} + static void ExcludeConstructorDiagnosticFixtures (Dictionary> methods) + { + var fixtureTypePrefix = "UserApp."; + var fixtureTypeNames = new HashSet (StringComparer.Ordinal) { + $"{fixtureTypePrefix}SignedUnsignedConstructorCollision, UserTypesFixture", + $"{fixtureTypePrefix}AliasedTypeConstructorCollision, UserTypesFixture", + $"{fixtureTypePrefix}GenericParameterConstructor`1, UserTypesFixture", + $"{fixtureTypePrefix}GenericInstantiationConstructor, UserTypesFixture", + $"{fixtureTypePrefix}ByRefConstructor, UserTypesFixture", + $"{fixtureTypePrefix}PointerConstructor, UserTypesFixture", + $"{fixtureTypePrefix}FunctionPointerConstructor, UserTypesFixture", + $"{fixtureTypePrefix}RectangularArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}MissingBaseConstructor, UserTypesFixture", + $"{fixtureTypePrefix}InvalidSuperArgumentsConstructor, UserTypesFixture", + }; + + foreach (var javaName in methods.Keys.ToList ()) { + methods [javaName].RemoveAll (group => fixtureTypeNames.Contains (group.ManagedName)); + if (methods [javaName].Count == 0) { + methods.Remove (javaName); + } + } + } } From bd19ba42c5961188633a5a58fef7e7f148b5d1a6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:06:54 +0200 Subject: [PATCH 21/45] [typemap] Validate nested constructor shapes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 25 ++++++--- .../TrimmableTypeMapBuildTests.cs | 52 +++++++++++++++++++ .../ConstructorParityTests.cs | 13 +++-- .../ScannerComparisonTests.cs | 4 ++ .../UserTypesFixture/UserTypes.cs | 20 +++++++ .../InvalidConstructors.cs | 28 ++++++++++ .../Scanner/ConstructorDetectionTests.cs | 4 ++ 7 files changed, 135 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index c4eeb304055..d5869118eb3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2956,13 +2956,7 @@ static bool TryValidateConstructorParameterShapes ( out string unsupportedParameterType) { foreach (var parameterType in parameterTypes) { - if (parameterType.GenericArguments.Count > 0 || - parameterType.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal) || - parameterType.ManagedTypeName == "delegate*" || - parameterType.ManagedTypeName.EndsWith ("&", StringComparison.Ordinal) || - parameterType.ManagedTypeName.EndsWith ("*", StringComparison.Ordinal) || - (parameterType.ManagedTypeName.EndsWith ("]", StringComparison.Ordinal) && - !parameterType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal))) { + if (IsUnsupportedConstructorParameterShape (parameterType)) { unsupportedParameterType = parameterType.DisplayName; return false; } @@ -2972,6 +2966,23 @@ static bool TryValidateConstructorParameterShapes ( return true; } + internal static bool IsUnsupportedConstructorParameterShape (TypeRefData parameterType) + { + string managedTypeName = parameterType.ManagedTypeName; + if (managedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + return IsUnsupportedConstructorParameterShape (parameterType with { + ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 2), + }); + } + + return parameterType.GenericArguments.Count > 0 || + managedTypeName.StartsWith ("!", StringComparison.Ordinal) || + managedTypeName == "delegate*" || + managedTypeName.EndsWith ("&", StringComparison.Ordinal) || + managedTypeName.EndsWith ("*", StringComparison.Ordinal) || + managedTypeName.EndsWith ("]", StringComparison.Ordinal); + } + string? TryGetConstructorJniDescriptor (TypeRefData parameterType) { if (parameterType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index a61bf1551e8..475bced31cc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -608,6 +608,58 @@ public ConstructorCollision (uint value) { } "Constructor diagnostics must be reported before partial Java output is written."); } + [TestCase ("rectangular-in-sz-array", false)] + [TestCase ("pointer-array", true)] + [TestCase ("function-pointer-array", true)] + public void Build_WithUnsupportedNestedConstructorParameter_FailsBeforeWritingTrimmableOutputs (string shape, bool isUnsafe) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var parameterType = shape switch { + "rectangular-in-sz-array" => "string[][,]", + "pointer-array" => "int*[]", + "function-pointer-array" => "delegate* unmanaged[]", + _ => throw new InvalidOperationException ($"Unknown nested constructor shape '{shape}'."), + }; + var proj = new XamarinAndroidApplicationProject { + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + if (isUnsafe) { + proj.SetProperty ("AllowUnsafeBlocks", "true"); + } + proj.Sources.Add (new BuildItem.Source ("NestedConstructorShape.cs") { + TextContent = () => $$""" + using Android.App; + using Java.Interop; + + namespace UnnamedProject; + + public {{(isUnsafe ? "unsafe " : "")}}class NestedConstructorShape : Activity + { + [Export (".ctor", SuperArgumentsString = "")] + public NestedConstructorShape ({{parameterType}} value) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"Build should fail for nested constructor parameter '{parameterType}'."); + StringAssertEx.Contains ("error XA4260", builder.LastBuildOutput); + Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("error XA4263", StringComparison.Ordinal)), + "Constructor structural shapes are owned by XA4260."); + + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + Assert.IsFalse (Directory.Exists (typemapDirectory) && Directory.EnumerateFiles (typemapDirectory, "*.java", SearchOption.AllDirectories).Any (), + "Constructor diagnostics must be reported before partial Java output is written."); + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs index 06d874c0517..e46e6066630 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs @@ -22,6 +22,7 @@ public void LegacyConstructorSignatureCollision_KeepsFirstSignature (string mana [InlineData ("UserApp.ByRefConstructor")] [InlineData ("UserApp.PointerConstructor")] [InlineData ("UserApp.FunctionPointerConstructor")] + [InlineData ("UserApp.FunctionPointerArrayConstructor")] public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) { var fixturePath = UserTypesFixturePath; @@ -30,13 +31,17 @@ public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) Assert.Single (constructors, c => c.JniSignature == "()V"); } - [Fact] - public void LegacyRectangularArrayConstructor_IsRepresentedAsJavaArray () + [Theory] + [InlineData ("UserApp.RectangularArrayConstructor", "([Ljava/lang/String;)V")] + [InlineData ("UserApp.NestedRectangularArrayConstructor", "([Ljava/lang/String;)V")] + [InlineData ("UserApp.PointerArrayConstructor", "([I)V")] + [InlineData ("UserApp.JaggedArrayConstructor", "([Ljava/lang/String;)V")] + public void LegacyArrayConstructor_UsesMeasuredJniSignature (string managedTypeName, string signature) { var fixturePath = UserTypesFixturePath; Assert.NotNull (fixturePath); - var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, "UserApp.RectangularArrayConstructor"); - Assert.Single (constructors, c => c.JniSignature == "([Ljava/lang/String;)V"); + var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName); + Assert.Single (constructors, c => c.JniSignature == signature); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index c83316d40bb..2b6f66a43ea 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -157,6 +157,10 @@ static void ExcludeConstructorDiagnosticFixtures (Dictionary[] value) { } + } + + public class JaggedArrayConstructor : Activity + { + public JaggedArrayConstructor (string[][] value) { } + } + [Register ("com/example/userapp/NoDefaultBase")] public class NoDefaultConstructorBase : Java.Lang.Object { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 36c04ef1c2f..56df2020d7d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -138,6 +138,34 @@ public class RectangularArrayCtorActivity : Android.App.Activity public RectangularArrayCtorActivity (string[,] value) { } } +[Register ("my/app/NestedRectangularArrayCtorActivity")] +public class NestedRectangularArrayCtorActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public NestedRectangularArrayCtorActivity (string[][,] value) : base (0) { } +} + +[Register ("my/app/PointerArrayCtorActivity")] +public unsafe class PointerArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public PointerArrayCtorActivity (int*[] value) { } +} + +[Register ("my/app/FunctionPointerArrayCtorActivity")] +public unsafe class FunctionPointerArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public FunctionPointerArrayCtorActivity (delegate* unmanaged[] value) { } +} + +[Register ("my/app/JaggedArrayCtorActivity")] +public class JaggedArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public JaggedArrayCtorActivity (string[][] value) { } +} + [Register ("my/app/NoDefaultBase", DoNotGenerateAcw = true)] public class NoDefaultBase : Java.Lang.Object { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 8a7525eb591..46541937042 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -166,6 +166,9 @@ public void UnsignedPrimitiveCtor_MapsCorrectly () [InlineData ("MyApp.PointerCtorActivity", "System.Int32*")] [InlineData ("MyApp.FunctionPointerCtorActivity", "delegate*")] [InlineData ("MyApp.RectangularArrayCtorActivity", "System.String[,]")] + [InlineData ("MyApp.NestedRectangularArrayCtorActivity", "System.String[,][]")] + [InlineData ("MyApp.PointerArrayCtorActivity", "System.Int32*[]")] + [InlineData ("MyApp.FunctionPointerArrayCtorActivity", "delegate*[]")] [InlineData ("MyApp.NonPublicExportUnsupportedActivity`1", "!0")] public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, string parameterType) { @@ -225,6 +228,7 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.ExplicitRegisterCompatibleBaseActivity")] [InlineData ("MyApp.JniSignatureCompatibleBaseActivity")] [InlineData ("MyApp.BindingPointerCtor")] + [InlineData ("MyApp.JaggedArrayCtorActivity")] [InlineData ("MyApp.ValidSuperExpressionActivity")] [InlineData ("MyApp.LexicalSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) From c7debf7d7eb1225ffce08c019ac995e1593161b1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:20:01 +0200 Subject: [PATCH 22/45] [tests] Remove vacuous export diagnostic assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 475bced31cc..76fc1020cb9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -652,8 +652,6 @@ public NestedConstructorShape ({{parameterType}} value) { } builder.ThrowOnBuildFailure = false; Assert.IsFalse (builder.Build (proj), $"Build should fail for nested constructor parameter '{parameterType}'."); StringAssertEx.Contains ("error XA4260", builder.LastBuildOutput); - Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("error XA4263", StringComparison.Ordinal)), - "Constructor structural shapes are owned by XA4260."); var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); Assert.IsFalse (Directory.Exists (typemapDirectory) && Directory.EnumerateFiles (typemapDirectory, "*.java", SearchOption.AllDirectories).Any (), From 11c4bea85f51ae3ec2b694020da733c7d1ea2853 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 16:45:51 +0200 Subject: [PATCH 23/45] [typemap] Suppress secondary constructor diagnostics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 54 ++++++++------- .../TrimmableTypeMapBuildTests.cs | 68 +++++++++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 22 ++++++ .../InvalidConstructors.cs | 26 +++++++ .../Scanner/ConstructorDetectionTests.cs | 6 ++ 5 files changed, 150 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index d5869118eb3..1075e58fc83 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -848,14 +848,9 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b ? declaringTypeName + methodName : $"{declaringTypeName}.{methodName}"; - for (int i = 0; i < sig.ParameterTypes.Length; i++) { - if (isConstructor && IsOwnedByConstructorDiagnostics (sig.ParameterTypes [i])) { - continue; - } - if (!HasExportSignatureMapping (sig.ParameterTypes [i], parameterKinds [i])) { - logger?.LogUnsupportedExportSignatureError (memberName, sig.ParameterTypes [i].DisplayName); - return false; - } + if (TryGetUnsupportedExportParameterType (sig.ParameterTypes, parameterKinds, isConstructor, out var unsupportedType)) { + logger?.LogUnsupportedExportSignatureError (memberName, unsupportedType); + return false; } if (isConstructor) { return true; @@ -867,6 +862,26 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b return true; } + bool TryGetUnsupportedExportParameterType ( + IReadOnlyList parameterTypes, + IReadOnlyList parameterKinds, + bool isConstructor, + out string unsupportedType) + { + for (int i = 0; i < parameterTypes.Count; i++) { + if (isConstructor && IsOwnedByConstructorDiagnostics (parameterTypes [i])) { + continue; + } + if (!HasExportSignatureMapping (parameterTypes [i], parameterKinds [i])) { + unsupportedType = parameterTypes [i].DisplayName; + return true; + } + } + + unsupportedType = ""; + return false; + } + /// /// PR #12567 owns XA4260 for signature shapes that no Java constructor can represent. /// This layer only adds XA4263 for unresolved types and invalid [ExportParameter] pairs. @@ -2862,6 +2877,10 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( if (IsActivationConstructor (signature.ParameterTypes)) { continue; } + if (exportInfo is not null && + TryGetUnsupportedExportParameterType (signature.ParameterTypes, exportInfo.ParameterKinds, isConstructor: true, out _)) { + continue; + } string jniSignature; string unsupportedParameterType; @@ -2956,7 +2975,7 @@ static bool TryValidateConstructorParameterShapes ( out string unsupportedParameterType) { foreach (var parameterType in parameterTypes) { - if (IsUnsupportedConstructorParameterShape (parameterType)) { + if (IsOwnedByConstructorDiagnostics (parameterType)) { unsupportedParameterType = parameterType.DisplayName; return false; } @@ -2966,23 +2985,6 @@ static bool TryValidateConstructorParameterShapes ( return true; } - internal static bool IsUnsupportedConstructorParameterShape (TypeRefData parameterType) - { - string managedTypeName = parameterType.ManagedTypeName; - if (managedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { - return IsUnsupportedConstructorParameterShape (parameterType with { - ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 2), - }); - } - - return parameterType.GenericArguments.Count > 0 || - managedTypeName.StartsWith ("!", StringComparison.Ordinal) || - managedTypeName == "delegate*" || - managedTypeName.EndsWith ("&", StringComparison.Ordinal) || - managedTypeName.EndsWith ("*", StringComparison.Ordinal) || - managedTypeName.EndsWith ("]", StringComparison.Ordinal); - } - string? TryGetConstructorJniDescriptor (TypeRefData parameterType) { if (parameterType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 76fc1020cb9..d69b5a4460b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -574,6 +574,74 @@ static string [] FindExportOutputs (string typemapDirectory, string acwMapFile, return outputs.ToArray (); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7004")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4263")] + public void Build_UnsupportedExportConstructorOverloads_ReportOnlyExportDiagnostics ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("UnsupportedExportConstructors.cs") { + TextContent = () => """ + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + public sealed class UnsupportedOne { } + public sealed class UnsupportedTwo { } + + [Register ("my/app/NoDefaultBase", DoNotGenerateAcw = true)] + public class NoDefaultBase : Java.Lang.Object + { + [Register (".ctor", "(I)V", "")] + public NoDefaultBase (int value) { } + } + + [Register ("my/app/UnsupportedExportConstructorOverloads")] + public class UnsupportedExportConstructorOverloads : NoDefaultBase + { + [Export (".ctor")] + public UnsupportedExportConstructorOverloads ( + [ExportParameter (ExportParameterKind.InputStream)] UnsupportedOne value) : base (0) { } + + [Export (".ctor")] + public UnsupportedExportConstructorOverloads ( + [ExportParameter (ExportParameterKind.InputStream)] UnsupportedTwo value) : base (0) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject unsupported exported constructors."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedOne'", builder.LastBuildOutput); + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedTwo'", builder.LastBuildOutput); + Assert.IsFalse (builder.LastBuildOutput.Any (line => + line.Contains ("Type 'UnnamedProject.UnsupportedExportConstructorOverloads'", StringComparison.Ordinal) && + (line.Contains ("error XA4259", StringComparison.Ordinal) || + line.Contains ("error XA4260", StringComparison.Ordinal) || + line.Contains ("error XA4261", StringComparison.Ordinal)))); + } + AssertNoExportOutputs (builder, "UnsupportedExportConstructorOverloads"); + } + [Test] public void Build_WithCollidingConstructorSignatures_FailsBeforeWritingTrimmableOutputs () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 9fc7706f729..ce17d9cb31c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -444,6 +444,28 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Assert.Contains (logMessages, message => message.StartsWith ("XA4260:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4261:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4262:", StringComparison.Ordinal)); + + var unsupportedExportMessages = logMessages + .Where (message => message.Contains ("MyApp.UnsupportedExportConstructorOverloadsActivity", StringComparison.Ordinal)) + .ToList (); + Assert.Equal (2, unsupportedExportMessages.Count); + Assert.All (unsupportedExportMessages, message => Assert.StartsWith ("XA4263:", message)); + + foreach (var typeName in new [] { + "GenericParameterCtorActivity", + "GenericInstantiationCtorActivity", + "ByRefCtorActivity", + "PointerCtorActivity", + "FunctionPointerCtorActivity", + "RectangularArrayCtorActivity", + "NestedRectangularArrayCtorActivity", + "PointerArrayCtorActivity", + "FunctionPointerArrayCtorActivity", + }) { + var messages = logMessages.Where (message => message.Contains ($"MyApp.{typeName}", StringComparison.Ordinal)).ToList (); + var message = Assert.Single (messages); + Assert.StartsWith ("XA4260:", message); + } } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 56df2020d7d..0d868ddc57a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -105,36 +105,42 @@ public AliasedTypeCollisionActivity (AliasTwo value) { } [Register ("my/app/GenericParameterCtorActivity")] public class GenericParameterCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public GenericParameterCtorActivity (T value) { } } [Register ("my/app/GenericInstantiationCtorActivity")] public class GenericInstantiationCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public GenericInstantiationCtorActivity (System.Collections.Generic.List value) { } } [Register ("my/app/ByRefCtorActivity")] public class ByRefCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public ByRefCtorActivity (ref int value) { } } [Register ("my/app/PointerCtorActivity")] public unsafe class PointerCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public PointerCtorActivity (int* value) { } } [Register ("my/app/FunctionPointerCtorActivity")] public unsafe class FunctionPointerCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public FunctionPointerCtorActivity (delegate* unmanaged value) { } } [Register ("my/app/RectangularArrayCtorActivity")] public class RectangularArrayCtorActivity : Android.App.Activity { + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] public RectangularArrayCtorActivity (string[,] value) { } } @@ -173,6 +179,26 @@ public class NoDefaultBase : Java.Lang.Object public NoDefaultBase (int value) { } } +public sealed class UnsupportedConstructorValueOne +{ +} + +public sealed class UnsupportedConstructorValueTwo +{ +} + +[Register ("my/app/UnsupportedExportConstructorOverloadsActivity")] +public class UnsupportedExportConstructorOverloadsActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + public UnsupportedExportConstructorOverloadsActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Java.Interop.Export (".ctor")] + public UnsupportedExportConstructorOverloadsActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + [Register ("my/app/MissingBaseCtorActivity")] public class MissingBaseCtorActivity : NoDefaultBase { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 46541937042..b3bc76c43c0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -247,6 +247,12 @@ public void ExportMappedConstructors_UseEffectiveJniSignaturesWithoutDiagnostics Assert.Contains ("(Ljava/util/List;)V", signatures); } + [Fact] + public void UnsupportedExportConstructors_HaveNoSecondaryConstructorDiagnostics () + { + Assert.Empty (ScanConstructorDiagnostics ("MyApp.UnsupportedExportConstructorOverloadsActivity")); + } + static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) => ScanPeer (managedTypeName).ConstructorDiagnostics; From 96693e77b079e0094f13fb3d22752fb7b22c2ff4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:03:38 +0200 Subject: [PATCH 24/45] [typemap] Collect independent validation errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 7 ++- .../TrimmableTypeMapBuildTests.cs | 54 +++++++++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 1 + .../InvalidConstructors.cs | 5 ++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5470034603d..d17c3ecd574 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -56,14 +56,13 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); - if (!ValidateConstructors (allPeers)) { - return new TrimmableTypeMapResult ([], [], allPeers); - } + bool validConstructors = ValidateConstructors (allPeers); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - if (!ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass)) { + bool validJavaNames = ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass); + if (!validConstructors || !validJavaNames) { return new TrimmableTypeMapResult ([], [], allPeers); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index d69b5a4460b..a835381a1a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -574,6 +574,60 @@ static string [] FindExportOutputs (string typemapDirectory, string acwMapFile, return outputs.ToArray (); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "JAVAC0000", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4262", "XA4258")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4262", "XA4258")] + public void Build_IndependentConstructorAndJavaNameDiagnostics_AreBothReported ( + string typeMapImplementation, + AndroidRuntime runtime, + string constructorCode, + string javaNameCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("IndependentDiagnostics.cs") { + TextContent = () => """ + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/InvalidConstructor")] + public class InvalidConstructor : Java.Lang.Object + { + [Export (".ctor", SuperArgumentsString = "p1 +")] + public InvalidConstructor (string value) { } + } + + [Register ("my/app/for")] + public class ReservedName : Java.Lang.Object { } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should report both independent errors."); + StringAssertEx.Contains ($"error {constructorCode}", builder.LastBuildOutput); + StringAssertEx.Contains ($"error {javaNameCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + AssertNoExportOutputs (builder, "InvalidConstructor"); + } else { + StringAssertEx.Contains ("InvalidConstructor.java", builder.LastBuildOutput); + StringAssertEx.Contains ("for.java", builder.LastBuildOutput); + } + } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4263")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index ce17d9cb31c..d2c6e4fffc1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -444,6 +444,7 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Assert.Contains (logMessages, message => message.StartsWith ("XA4260:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4261:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4262:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); var unsupportedExportMessages = logMessages .Where (message => message.Contains ("MyApp.UnsupportedExportConstructorOverloadsActivity", StringComparison.Ordinal)) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 0d868ddc57a..ef7225f1df5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -8,6 +8,11 @@ public enum ConstructorKind None, } +[Register ("my/app/for")] +public class ReservedJavaNameActivity : Android.App.Activity +{ +} + [Register ("my/app/EnumCtorActivity")] public class EnumCtorActivity : Android.App.Activity { From a35870a884969febb7c2db166aa340a91a4a9f64 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:04:13 +0200 Subject: [PATCH 25/45] [typemap] Track rejected export constructors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 28 ++++++++++++------- .../TrimmableTypeMapGeneratorTests.cs | 14 ++++++---- .../InvalidConstructors.cs | 28 +++++++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 9 ++++-- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 1075e58fc83..ef62128d3c2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -395,8 +395,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // - Interface types don't implement other interfaces' methods in JCWs List? marshalMethods = null; List? exportFields = null; + var rejectedExportConstructors = new HashSet (); if (!doNotGenerateAcw || collectMarshalMethodsForNonAcw) { - (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); + (marshalMethods, exportFields, rejectedExportConstructors) = + CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); } // Resolve activation constructor @@ -435,7 +437,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A JavaConstructors = marshalMethods is not null ? BuildJavaConstructors (marshalMethods, typeDef, index) : [], ConstructorDiagnostics = doNotGenerateAcw || marshalMethods is null ? [] - : AnalyzeConstructorDiagnostics (typeDef, index, marshalMethods), + : AnalyzeConstructorDiagnostics (typeDef, index, marshalMethods, rejectedExportConstructors), JavaFields = exportFields ?? [], ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, @@ -686,11 +688,15 @@ static void SkipArrayShape (ref BlobReader reader) } } - (List, List) CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index, bool detectBaseOverrides) + (List, List, HashSet) CollectMarshalMethods ( + TypeDefinition typeDef, + AssemblyIndex index, + bool detectBaseOverrides) { var methods = new List (); var fields = new List (); HashSet? registeredMethodKeys = detectBaseOverrides ? new (StringComparer.Ordinal) : null; + var rejectedExportConstructors = new HashSet (); bool isGenericType = typeDef.GetGenericParameters ().Count > 0; // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them @@ -705,6 +711,9 @@ static void SkipArrayShape (ref BlobReader reader) continue; } if (!ValidateExportSignature (methodDef, index, isGenericType)) { + if (methodName == ".ctor") { + rejectedExportConstructors.Add (methodHandle); + } continue; } @@ -768,7 +777,7 @@ static void SkipArrayShape (ref BlobReader reader) CollectBaseConstructorChain (typeDef, index, methods); } - return (methods, fields); + return (methods, fields, rejectedExportConstructors); } static bool IsExportFieldAttribute (CustomAttribute attribute, AssemblyIndex index) @@ -2853,7 +2862,8 @@ List GetSupportedPublicConstructors (TypeDefinition typeD IReadOnlyList AnalyzeConstructorDiagnostics ( TypeDefinition typeDef, AssemblyIndex index, - IReadOnlyList marshalMethods) + IReadOnlyList marshalMethods, + HashSet rejectedExportConstructors) { var diagnostics = new List (); var signatures = new Dictionary (StringComparer.Ordinal); @@ -2866,6 +2876,9 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( index.Reader.GetString (methodDef.Name) != ".ctor") { continue; } + if (rejectedExportConstructors.Contains (methodHandle)) { + continue; + } bool hasExplicitRegistration = TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo); if ((methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public && @@ -2877,11 +2890,6 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( if (IsActivationConstructor (signature.ParameterTypes)) { continue; } - if (exportInfo is not null && - TryGetUnsupportedExportParameterType (signature.ParameterTypes, exportInfo.ParameterKinds, isConstructor: true, out _)) { - continue; - } - string jniSignature; string unsupportedParameterType; if (registerInfo?.Signature is string registeredSignature) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index d2c6e4fffc1..af584e7e140 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -446,11 +446,15 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Assert.Contains (logMessages, message => message.StartsWith ("XA4262:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); - var unsupportedExportMessages = logMessages - .Where (message => message.Contains ("MyApp.UnsupportedExportConstructorOverloadsActivity", StringComparison.Ordinal)) - .ToList (); - Assert.Equal (2, unsupportedExportMessages.Count); - Assert.All (unsupportedExportMessages, message => Assert.StartsWith ("XA4263:", message)); + foreach (var typeName in new [] { + "UnsupportedExportConstructorOverloadsActivity", + "RegisterBeforeExportActivity", + "ExportBeforeRegisterActivity", + }) { + var messages = logMessages.Where (message => message.Contains ($"MyApp.{typeName}", StringComparison.Ordinal)).ToList (); + Assert.Equal (2, messages.Count); + Assert.All (messages, message => Assert.StartsWith ("XA4263:", message)); + } foreach (var typeName in new [] { "GenericParameterCtorActivity", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index ef7225f1df5..2a6b07eff28 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -204,6 +204,34 @@ public UnsupportedExportConstructorOverloadsActivity ( [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } } +[Register ("my/app/RegisterBeforeExportActivity")] +public class RegisterBeforeExportActivity : NoDefaultBase +{ + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + +[Register ("my/app/ExportBeforeRegisterActivity")] +public class ExportBeforeRegisterActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + public ExportBeforeRegisterActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + public ExportBeforeRegisterActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + [Register ("my/app/MissingBaseCtorActivity")] public class MissingBaseCtorActivity : NoDefaultBase { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index b3bc76c43c0..eac798f8f5c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -247,10 +247,13 @@ public void ExportMappedConstructors_UseEffectiveJniSignaturesWithoutDiagnostics Assert.Contains ("(Ljava/util/List;)V", signatures); } - [Fact] - public void UnsupportedExportConstructors_HaveNoSecondaryConstructorDiagnostics () + [Theory] + [InlineData ("MyApp.UnsupportedExportConstructorOverloadsActivity")] + [InlineData ("MyApp.RegisterBeforeExportActivity")] + [InlineData ("MyApp.ExportBeforeRegisterActivity")] + public void UnsupportedExportConstructors_HaveNoSecondaryConstructorDiagnostics (string managedTypeName) { - Assert.Empty (ScanConstructorDiagnostics ("MyApp.UnsupportedExportConstructorOverloadsActivity")); + Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); } static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) => From 7df69a6cf71a6d027be3beec4e89d4f0af62eedf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:05:06 +0200 Subject: [PATCH 26/45] [typemap] Preserve constructor type identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 9 +++---- .../InvalidConstructorFixtures.csproj | 5 +++- .../InvalidConstructors.cs | 25 +++++++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 9 +++++++ .../TestFixtures/TestTypes.cs | 10 ++++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index ef62128d3c2..0d47d05c49c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2866,7 +2866,7 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( HashSet rejectedExportConstructors) { var diagnostics = new List (); - var signatures = new Dictionary (StringComparer.Ordinal); + var signatures = new Dictionary> (StringComparer.Ordinal); var baseConstructors = CollectBaseRegisteredCtors (typeDef, index); bool hasParameterlessBaseConstructor = baseConstructors.Any (c => c.RegisterInfo.Signature == "()V"); @@ -2910,15 +2910,14 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( continue; } - var managedSignature = string.Join (",", signature.ParameterTypes.Select (p => p.DisplayName)); if (signatures.TryGetValue (jniSignature, out var existingManagedSignature) && - !string.Equals (existingManagedSignature, managedSignature, StringComparison.Ordinal)) { + !existingManagedSignature.SequenceEqual (signature.ParameterTypes)) { diagnostics.Add (new ConstructorDiagnosticInfo { Kind = ConstructorDiagnosticKind.AmbiguousJniSignature, Detail = jniSignature, }); } else { - signatures [jniSignature] = managedSignature; + signatures [jniSignature] = signature.ParameterTypes; } if (exportInfo?.SuperArgumentsString is string superArgumentsString && @@ -3013,7 +3012,7 @@ static bool TryValidateConstructorParameterShapes ( return enumDescriptor; } - return TryResolveJniObjectDescriptor (parameterType.ManagedTypeName); + return TryResolveJniObjectDescriptor (parameterType); } static bool IsActivationConstructor (IReadOnlyList parameterTypes) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj index 31621f08c52..3876e17b8d1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj @@ -5,11 +5,14 @@ latest enable true + $(NoWarn);CS0436 false - + + global,TestFixtures + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 2a6b07eff28..4710bbf668a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -1,5 +1,8 @@ +extern alias TestFixtures; + using System; using Android.Runtime; +using ExternalCollisionParameter = TestFixtures::MyApp.CrossAssemblyCollisionParameter; namespace MyApp; @@ -107,6 +110,28 @@ public AliasedTypeCollisionActivity (AliasOne value) { } public AliasedTypeCollisionActivity (AliasTwo value) { } } +[Register ("my/app/CrossAssemblyParameter", DoNotGenerateAcw = true)] +public class CrossAssemblyCollisionParameter : Java.Lang.Object +{ +} + +public sealed class CrossAssemblyBorrowedParameter +{ +} + +[Register ("my/app/CrossAssemblyCollisionActivity")] +public class CrossAssemblyCollisionActivity : Android.App.Activity +{ + public CrossAssemblyCollisionActivity (CrossAssemblyCollisionParameter value) { } + public CrossAssemblyCollisionActivity (ExternalCollisionParameter value) { } +} + +[Register ("my/app/CrossAssemblyBorrowActivity")] +public class CrossAssemblyBorrowActivity : Android.App.Activity +{ + public CrossAssemblyBorrowActivity (CrossAssemblyBorrowedParameter value) { } +} + [Register ("my/app/GenericParameterCtorActivity")] public class GenericParameterCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index eac798f8f5c..73fca06cd66 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -184,6 +184,7 @@ public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, [InlineData ("MyApp.ExplicitRegisterCollisionActivity", "(I)V")] [InlineData ("MyApp.JniSignatureCollisionActivity", "(I)V")] [InlineData ("MyApp.NonPublicExportCollisionActivity", "(I)V")] + [InlineData ("MyApp.CrossAssemblyCollisionActivity", "(Lmy/app/CrossAssemblyParameter;)V")] public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, string jniSignature) { var diagnostics = ScanConstructorDiagnostics (managedTypeName); @@ -192,6 +193,14 @@ public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, Assert.Equal (jniSignature, diagnostic.Detail); } + [Fact] + public void ConstructorDescriptor_DoesNotBorrowSameNamedTypeFromAnotherAssembly () + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.CrossAssemblyBorrowActivity")); + Assert.Equal (ConstructorDiagnosticKind.UnsupportedParameterType, diagnostic.Kind); + Assert.Equal ("MyApp.CrossAssemblyBorrowedParameter", diagnostic.Detail); + } + [Fact] public void MissingBaseConstructor_IsDiagnosed () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a27e7464572..0703e88fd2f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1368,6 +1368,16 @@ protected UnsignedParamActivity (IntPtr handle, JniHandleOwnership transfer) : b public UnsignedParamActivity (ushort a, uint b, ulong c) { } } + + [Register ("my/app/CrossAssemblyParameter", DoNotGenerateAcw = true)] + public class CrossAssemblyCollisionParameter : Java.Lang.Object + { + } + + [Register ("my/app/BorrowedParameter", DoNotGenerateAcw = true)] + public class CrossAssemblyBorrowedParameter : Java.Lang.Object + { + } } namespace MyApp.Generic From cdd301149c1bd5d2a442e49b87bf7facb546b4ad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:06:41 +0200 Subject: [PATCH 27/45] [typemap] Recognize Java lambda parameters Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 81 ++++++++++++++++++- .../InvalidConstructors.cs | 21 +++++ .../Scanner/ConstructorDetectionTests.cs | 3 + 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 0d47d05c49c..e4ec122a68b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -3035,6 +3035,8 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa bool inLineComment = false; bool inBlockComment = false; bool escaped = false; + var lambdaParameterScopes = new Dictionary (StringComparer.Ordinal); + char precedingSignificantCharacter = '\0'; char previousSignificantCharacter = '\0'; for (int i = 0; i < superArgumentsString.Length; i++) { @@ -3062,6 +3064,7 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa } else if ((inString && current == '"') || (inCharacter && current == '\'')) { inString = false; inCharacter = false; + precedingSignificantCharacter = previousSignificantCharacter; previousSignificantCharacter = current; } continue; @@ -3086,8 +3089,10 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa if (superArgumentsString [i] != 'p' || i + 1 >= superArgumentsString.Length || !char.IsDigit (superArgumentsString [i + 1]) || (i > 0 && IsJavaIdentifierCharacter (superArgumentsString [i - 1])) || - previousSignificantCharacter == '.') { + previousSignificantCharacter == '.' || + (precedingSignificantCharacter == ':' && previousSignificantCharacter == ':')) { if (!char.IsWhiteSpace (current)) { + precedingSignificantCharacter = previousSignificantCharacter; previousSignificantCharacter = current; } continue; @@ -3100,18 +3105,92 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa if (end < superArgumentsString.Length && IsJavaIdentifierCharacter (superArgumentsString [end])) { continue; } + var parameterName = superArgumentsString.Substring (i, end - i); + if (TryGetLambdaBodyEnd (superArgumentsString, end, out var lambdaBodyEnd)) { + if (!lambdaParameterScopes.TryGetValue (parameterName, out var existingEnd) || lambdaBodyEnd > existingEnd) { + lambdaParameterScopes [parameterName] = lambdaBodyEnd; + } + precedingSignificantCharacter = previousSignificantCharacter; + previousSignificantCharacter = superArgumentsString [end - 1]; + i = end - 1; + continue; + } else if (lambdaParameterScopes.TryGetValue (parameterName, out var scopeEnd) && i < scopeEnd) { + precedingSignificantCharacter = previousSignificantCharacter; + previousSignificantCharacter = superArgumentsString [end - 1]; + i = end - 1; + continue; + } var parameterToken = superArgumentsString.Substring (i + 1, end - i - 1); if ((parameterToken.Length > 1 && parameterToken [0] == '0') || !int.TryParse (parameterToken, out var parameterIndex) || parameterIndex >= parameterCount) { return false; } + precedingSignificantCharacter = previousSignificantCharacter; previousSignificantCharacter = superArgumentsString [end - 1]; i = end - 1; } return true; } + static bool TryGetLambdaBodyEnd (string value, int tokenEnd, out int bodyEnd) + { + int arrow = tokenEnd; + while (arrow < value.Length && char.IsWhiteSpace (value [arrow])) { + arrow++; + } + if (arrow + 1 >= value.Length || value [arrow] != '-' || value [arrow + 1] != '>') { + bodyEnd = 0; + return false; + } + + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int i = arrow + 2; i < value.Length; i++) { + switch (value [i]) { + case '(': + parentheses++; + break; + case '[': + brackets++; + break; + case '{': + braces++; + break; + case ')': + if (parentheses == 0 && brackets == 0 && braces == 0) { + bodyEnd = i; + return true; + } + parentheses--; + break; + case ']': + if (parentheses == 0 && brackets == 0 && braces == 0) { + bodyEnd = i; + return true; + } + brackets--; + break; + case '}': + if (parentheses == 0 && brackets == 0 && braces == 0) { + bodyEnd = i; + return true; + } + braces--; + break; + case ',': + if (parentheses == 0 && brackets == 0 && braces == 0) { + bodyEnd = i; + return true; + } + break; + } + } + bodyEnd = value.Length; + return true; + } + static bool IsJavaIdentifierCharacter (char value) => char.IsLetterOrDigit (value) || value == '_' || value == '$'; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 4710bbf668a..7eed1fc97d6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -298,6 +298,27 @@ public class LexicalSuperArgumentsActivity : Android.App.Activity public LexicalSuperArgumentsActivity (string value) { } } +[Register ("my/app/LambdaSuperArgumentsActivity")] +public class LambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> p1")] + public LambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/MethodReferenceSuperArgumentsActivity")] +public class MethodReferenceSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1")] + public MethodReferenceSuperArgumentsActivity () { } +} + +[Register ("my/app/LambdaWithBareParameterActivity")] +public class LambdaWithBareParameterActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> p1, p1")] + public LambdaWithBareParameterActivity () { } +} + [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 73fca06cd66..fd9fd2f6c60 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -224,6 +224,7 @@ public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed [InlineData ("MyApp.InvalidSuperArgumentsActivity", "p1")] [InlineData ("MyApp.NonCanonicalSuperArgumentZeroActivity", "p00")] [InlineData ("MyApp.NonCanonicalSuperArgumentOneActivity", "p01")] + [InlineData ("MyApp.LambdaWithBareParameterActivity", "p1 -> p1, p1")] public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) { var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); @@ -240,6 +241,8 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.JaggedArrayCtorActivity")] [InlineData ("MyApp.ValidSuperExpressionActivity")] [InlineData ("MyApp.LexicalSuperArgumentsActivity")] + [InlineData ("MyApp.LambdaSuperArgumentsActivity")] + [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) { Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); From d4c2bf53a8b2f5339f99aa2581c34779aab36ae9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:14:13 +0200 Subject: [PATCH 28/45] [tests] Measure Java super argument forms Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapBuildTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index a835381a1a1..b88dc4dfcef 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -628,6 +628,93 @@ public class ReservedName : Java.Lang.Object { } } } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] + public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( + string typeMapImplementation, + AndroidRuntime runtime, + string shape, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var superArguments = shape switch { + "lambda" => "p1 -> p1", + "method-reference" => "Helper::p1", + "invalid-bare" => "p1 -> p1, p1", + _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), + }; + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("SuperArgumentsPeer.cs") { + TextContent = () => $$""" + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/SuperArgumentsBase", DoNotGenerateAcw = true)] + public class SuperArgumentsBase : Java.Lang.Object + { + [Register (".ctor", "(Ljava/util/function/Function;Ljava/lang/Object;)V", "")] + public SuperArgumentsBase () { } + } + + [Register ("my/app/SuperArgumentsPeer")] + public class SuperArgumentsPeer : SuperArgumentsBase + { + [Export (".ctor", SuperArgumentsString = "{{superArguments}}")] + public SuperArgumentsPeer () { } + } + """, + }); + proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("my\\app\\SuperArgumentsBase.java") { + Encoding = Encoding.ASCII, + TextContent = () => """ + package my.app; + + public class SuperArgumentsBase { + public SuperArgumentsBase ( + java.util.function.Function function) {} + public SuperArgumentsBase ( + java.util.function.Supplier supplier) {} + public SuperArgumentsBase ( + java.util.function.Function function, + Object value) {} + + public static class Helper { + public static String p1 () { return ""; } + } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + var succeeded = builder.Build (proj); + if (expectedCode == "success") { + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should compile {shape} super arguments."); + } else { + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject the bare p1 reference."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + AssertNoExportOutputs (builder, "SuperArgumentsPeer"); + } + } + } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4263")] From 91098ed6fcf1fc3825dd48d8c90dc95eddc044f9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:18:42 +0200 Subject: [PATCH 29/45] [typemap] Preserve framework constructor compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 2 +- .../TrimmableTypeMapGeneratorTests.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index d17c3ecd574..7daf6188d73 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -89,7 +89,7 @@ internal bool ValidateConstructors (IReadOnlyList peers) { bool valid = true; foreach (var peer in peers) { - if (!ShouldGenerateJcw (peer)) { + if (peer.IsFrameworkAssembly || !ShouldGenerateJcw (peer)) { continue; } foreach (var diagnostic in peer.ConstructorDiagnostics) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index af584e7e140..629b61bf0bf 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -129,6 +129,29 @@ public void ValidateConstructors_LogsCodedError ( Assert.Contains (logMessages, message => message.StartsWith ($"{errorCode}:", StringComparison.Ordinal)); } + [Fact] + public void ValidateConstructors_IgnoresFrameworkPeers () + { + var peer = new JavaPeerInfo { + JavaName = "android/example/FrameworkType", + CompatJniName = "android/example/FrameworkType", + ManagedTypeName = "Android.Example.FrameworkType", + ManagedTypeNamespace = "Android.Example", + ManagedTypeShortName = "FrameworkType", + AssemblyName = "Mono.Android", + IsFrameworkAssembly = true, + ConstructorDiagnostics = [ + new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.UnsupportedParameterType, + Detail = "System.Object", + }, + ], + }; + + Assert.True (CreateGenerator ().ValidateConstructors ([peer])); + Assert.Empty (logMessages); + } + [Fact] public void ValidateJavaNames_ContextualKeywordInPackage_IsValid () { From c3196080adbee2b0fde75acda8069abd24df5df7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:50:17 +0200 Subject: [PATCH 30/45] [typemap] Preserve explicit export metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 29 +++++++++-------- .../InvalidConstructors.cs | 32 +++++++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 15 +++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index e4ec122a68b..9205d6303cd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1908,39 +1908,40 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { + RegisterInfo? explicitRegisterInfo = null; + RegisterInfo? exportRegisterInfo = null; exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); var attrName = index.GetCustomAttributeName (ca); - if (attrName == "RegisterAttribute") { - registerInfo = index.ParseRegisterAttribute (ca); - return true; + if (attrName == "RegisterAttribute" && explicitRegisterInfo is null) { + explicitRegisterInfo = index.ParseRegisterAttribute (ca); + continue; } - if (IsExportAttribute (ca, index)) { - (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); - return true; + if (IsExportAttribute (ca, index) && exportInfo is null) { + (exportRegisterInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + continue; } - if (IsExportFieldAttribute (ca, index)) { - (registerInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); - return true; + if (IsExportFieldAttribute (ca, index) && exportInfo is null) { + (exportRegisterInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); + continue; } // JI-style constructor registration: [JniConstructorSignature("()V")] // Single arg = JNI signature; name is always ".ctor", connector is empty. - if (attrName == "JniConstructorSignatureAttribute") { + if (attrName == "JniConstructorSignatureAttribute" && explicitRegisterInfo is null) { var value = index.DecodeAttribute (ca); var jniSignature = value.FixedArguments.Length > 0 ? (string?)value.FixedArguments [0].Value : null; if (jniSignature is not null) { - registerInfo = new RegisterInfo { JniName = ".ctor", Signature = jniSignature, Connector = "", DoNotGenerateAcw = false }; - return true; + explicitRegisterInfo = new RegisterInfo { JniName = ".ctor", Signature = jniSignature, Connector = "", DoNotGenerateAcw = false }; } } } - registerInfo = null; - return false; + registerInfo = explicitRegisterInfo ?? exportRegisterInfo; + return registerInfo is not null; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 7eed1fc97d6..279ef3f0a29 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -77,6 +77,38 @@ public class JniSignatureCompatibleBaseActivity : IntSignatureBase public JniSignatureCompatibleBaseActivity (uint value) : base ((int)value) { } } +[Register ("my/app/RegisterBeforeExportValidActivity")] +public class RegisterBeforeExportValidActivity : IntSignatureBase +{ + [Register (".ctor", "(I)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportValidActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ExportBeforeRegisterValidActivity")] +public class ExportBeforeRegisterValidActivity : IntSignatureBase +{ + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(I)V", "")] + public ExportBeforeRegisterValidActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/JniBeforeExportValidActivity")] +public class JniBeforeExportValidActivity : IntSignatureBase +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + [Java.Interop.Export (".ctor")] + public JniBeforeExportValidActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ExportBeforeJniValidActivity")] +public class ExportBeforeJniValidActivity : IntSignatureBase +{ + [Java.Interop.Export (".ctor")] + [Java.Interop.JniConstructorSignature ("(I)V")] + public ExportBeforeJniValidActivity (uint value) : base ((int)value) { } +} + [Register ("my/app/BindingPointerCtor", DoNotGenerateAcw = true)] public unsafe class BindingPointerCtor : Java.Lang.Object { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index fd9fd2f6c60..ac18733194a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -237,6 +237,10 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.ExplicitPointerCtorActivity")] [InlineData ("MyApp.ExplicitRegisterCompatibleBaseActivity")] [InlineData ("MyApp.JniSignatureCompatibleBaseActivity")] + [InlineData ("MyApp.RegisterBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeRegisterValidActivity")] + [InlineData ("MyApp.JniBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeJniValidActivity")] [InlineData ("MyApp.BindingPointerCtor")] [InlineData ("MyApp.JaggedArrayCtorActivity")] [InlineData ("MyApp.ValidSuperExpressionActivity")] @@ -248,6 +252,17 @@ public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTy Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); } + [Theory] + [InlineData ("MyApp.RegisterBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeRegisterValidActivity")] + [InlineData ("MyApp.JniBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeJniValidActivity")] + public void ExplicitExportConstructor_PreservesRegistrationAndBaseForwarding (string managedTypeName) + { + var constructor = Assert.Single (ScanPeer (managedTypeName).JavaConstructors, c => c.JniSignature == "(I)V"); + Assert.Null (constructor.SuperArgumentsString); + } + [Fact] public void ExportMappedConstructors_UseEffectiveJniSignaturesWithoutDiagnostics () { From d952cb4efba04353500d89e1a71b849c26f6a18a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:51:42 +0200 Subject: [PATCH 31/45] [typemap] Validate primitive constructor identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 39 +++++++++++++------ .../InvalidConstructorFixtures.csproj | 3 ++ .../InvalidConstructors.cs | 24 ++++++++++++ .../LookalikeConstructorTypes.csproj | 13 +++++++ .../LookalikeTypes.cs | 33 ++++++++++++++++ ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 5 +++ .../Scanner/ConstructorDetectionTests.cs | 14 +++++++ 7 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 9205d6303cd..8f0a953a2be 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1229,7 +1229,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // then delegates to nctor_N(...) which handles the args on the managed side. // This matches legacy CecilImporter behavior (CecilImporter.cs:394-397). if (hasParameterlessBaseCtor) { - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); var jniSignature = BuildJniCtorSignature (sig); if (jniSignature is not null && !alreadyRegisteredSignatures.Contains (jniSignature)) { methods.Add (new MarshalMethodInfo { @@ -1247,7 +1247,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, } } - string? BuildJniCtorSignature (MethodSignature sig) + string? BuildJniCtorSignature (MethodSignature sig) { var sb = new System.Text.StringBuilder (); sb.Append ('('); @@ -1256,7 +1256,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // System.Object, System.Action, etc.). ManagedTypeToJniDescriptor maps // these to "Ljava/lang/Object;" by default, but legacy would reject the // whole ctor. Use the nullable variant to match legacy behavior. - var jniType = ManagedTypeToJniDescriptorOrNull (param); + var jniType = TryGetConstructorJniDescriptor (param); if (jniType is null) { return null; } @@ -2167,7 +2167,7 @@ bool TryManagedTypeToJniDescriptor ( return false; } - var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); + var primitive = TryGetPrimitiveJniDescriptor (managedType); if (primitive is not null) { descriptor = primitive; return true; @@ -2273,6 +2273,21 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para }; } + string? TryGetPrimitiveJniDescriptor (TypeRefData managedType) + { + var descriptor = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); + return descriptor is not null && + IsSpecialManagedType ( + managedType, + managedType.ManagedTypeName, + "System.Runtime", + "System.Private.CoreLib", + "mscorlib", + "netstandard") + ? descriptor + : null; + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index, TypeRefData? currentTypeRef = null) { var cacheKey = (currentTypeRef?.DisplayName ?? typeName, index.AssemblyName); @@ -3003,7 +3018,7 @@ static bool TryValidateConstructorParameterShapes ( return elementDescriptor is null ? null : $"[{elementDescriptor}"; } - var primitiveDescriptor = TryGetPrimitiveJniDescriptor (parameterType.ManagedTypeName); + var primitiveDescriptor = TryGetPrimitiveJniDescriptor (parameterType); if (primitiveDescriptor is not null) { return primitiveDescriptor; } @@ -3016,17 +3031,19 @@ static bool TryValidateConstructorParameterShapes ( return TryResolveJniObjectDescriptor (parameterType); } - static bool IsActivationConstructor (IReadOnlyList parameterTypes) + bool IsActivationConstructor (IReadOnlyList parameterTypes) { if (parameterTypes.Count != 2) { return false; } - return (parameterTypes [0].ManagedTypeName == "System.IntPtr" && - parameterTypes [1].ManagedTypeName == "Android.Runtime.JniHandleOwnership") || - ((parameterTypes [0].ManagedTypeName == "Java.Interop.JniObjectReference&" || - parameterTypes [0].ManagedTypeName == "Java.Interop.JniObjectReference") && - parameterTypes [1].ManagedTypeName == "Java.Interop.JniObjectReferenceOptions"); + return (IsSpecialManagedType (parameterTypes [0], "System.IntPtr", "System.Runtime", "System.Private.CoreLib", "mscorlib") && + IsSpecialManagedType (parameterTypes [1], "Android.Runtime.JniHandleOwnership", "Mono.Android")) || + (IsSpecialManagedType ( + parameterTypes [0] with { ManagedTypeName = parameterTypes [0].ManagedTypeName.TrimEnd ('&') }, + "Java.Interop.JniObjectReference", + "Java.Interop") && + IsSpecialManagedType (parameterTypes [1], "Java.Interop.JniObjectReferenceOptions", "Java.Interop")); } static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj index 3876e17b8d1..90c17710b7b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj @@ -13,6 +13,9 @@ global,TestFixtures + + Lookalikes + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 279ef3f0a29..40e6211846a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -1,8 +1,14 @@ extern alias TestFixtures; +extern alias Lookalikes; using System; using Android.Runtime; using ExternalCollisionParameter = TestFixtures::MyApp.CrossAssemblyCollisionParameter; +using LookalikeInt32 = Lookalikes::System.Int32; +using LookalikeIntPtr = Lookalikes::System.IntPtr; +using LookalikeJniObjectReference = Lookalikes::Java.Interop.JniObjectReference; +using LookalikeJniObjectReferenceOptions = Lookalikes::Java.Interop.JniObjectReferenceOptions; +using LookalikeOwnership = Lookalikes::Android.Runtime.JniHandleOwnership; namespace MyApp; @@ -164,6 +170,24 @@ public class CrossAssemblyBorrowActivity : Android.App.Activity public CrossAssemblyBorrowActivity (CrossAssemblyBorrowedParameter value) { } } +[Register ("my/app/PrimitiveLookalikeActivity")] +public class PrimitiveLookalikeActivity : Android.App.Activity +{ + public PrimitiveLookalikeActivity (LookalikeInt32 value) { } +} + +[Register ("my/app/XamarinActivationLookalikeActivity")] +public class XamarinActivationLookalikeActivity : Android.App.Activity +{ + public XamarinActivationLookalikeActivity (LookalikeIntPtr handle, LookalikeOwnership ownership) { } +} + +[Register ("my/app/JniActivationLookalikeActivity")] +public class JniActivationLookalikeActivity : Android.App.Activity +{ + public JniActivationLookalikeActivity (LookalikeJniObjectReference reference, LookalikeJniObjectReferenceOptions options) { } +} + [Register ("my/app/GenericParameterCtorActivity")] public class GenericParameterCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj new file mode 100644 index 00000000000..0b655968f54 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + enable + false + + + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs new file mode 100644 index 00000000000..9a0b4966699 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs @@ -0,0 +1,33 @@ +namespace System +{ + [Android.Runtime.Register ("my/app/LookalikeInt32", DoNotGenerateAcw = true)] + public class Int32 : Java.Lang.Object + { + } + + [Android.Runtime.Register ("my/app/LookalikeIntPtr", DoNotGenerateAcw = true)] + public class IntPtr : Java.Lang.Object + { + } +} + +namespace Android.Runtime +{ + [Register ("my/app/LookalikeOwnership", DoNotGenerateAcw = true)] + public class JniHandleOwnership : Java.Lang.Object + { + } +} + +namespace Java.Interop +{ + [Android.Runtime.Register ("my/app/LookalikeReference", DoNotGenerateAcw = true)] + public class JniObjectReference : Java.Lang.Object + { + } + + [Android.Runtime.Register ("my/app/LookalikeOptions", DoNotGenerateAcw = true)] + public class JniObjectReferenceOptions : Java.Lang.Object + { + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 2a03ed4e05d..9c91a0ef378 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -15,6 +15,7 @@ + @@ -35,6 +36,9 @@ false + + false + @@ -43,6 +47,7 @@ <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" /> <_TestFixtureFiles Include="InvalidConstructorFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\InvalidConstructorFixtures.dll" /> + <_TestFixtureFiles Include="LookalikeConstructorTypes\bin\$(Configuration)\$(DotNetStableTargetFramework)\LookalikeConstructorTypes.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index ac18733194a..d070eee2cb9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -201,6 +201,17 @@ public void ConstructorDescriptor_DoesNotBorrowSameNamedTypeFromAnotherAssembly Assert.Equal ("MyApp.CrossAssemblyBorrowedParameter", diagnostic.Detail); } + [Theory] + [InlineData ("MyApp.PrimitiveLookalikeActivity", "(Lmy/app/LookalikeInt32;)V")] + [InlineData ("MyApp.XamarinActivationLookalikeActivity", "(Lmy/app/LookalikeIntPtr;Lmy/app/LookalikeOwnership;)V")] + [InlineData ("MyApp.JniActivationLookalikeActivity", "(Lmy/app/LookalikeReference;Lmy/app/LookalikeOptions;)V")] + public void PrimitiveAndActivationLookalikes_UseRegisteredJavaDescriptors (string managedTypeName, string jniSignature) + { + var peer = ScanPeer (managedTypeName); + Assert.Empty (peer.ConstructorDiagnostics); + Assert.Contains (peer.JavaConstructors, constructor => constructor.JniSignature == jniSignature); + } + [Fact] public void MissingBaseConstructor_IsDiagnosed () { @@ -293,12 +304,15 @@ static JavaPeerInfo ScanPeer (string managedTypeName) ?? throw new InvalidOperationException ("Cannot determine test assembly directory."); using var fixtureReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var invalidFixturePath = Path.Combine (testAssemblyDir, "InvalidConstructorFixtures.dll"); + var lookalikeFixturePath = Path.Combine (testAssemblyDir, "LookalikeConstructorTypes.dll"); using var invalidFixtureReader = new PEReader (File.OpenRead (invalidFixturePath)); + using var lookalikeFixtureReader = new PEReader (File.OpenRead (lookalikeFixturePath)); var fixtureMetadata = fixtureReader.GetMetadataReader (); var invalidFixtureMetadata = invalidFixtureReader.GetMetadataReader (); var assemblies = new [] { (fixtureMetadata.GetString (fixtureMetadata.GetAssemblyDefinition ().Name), fixtureReader), (invalidFixtureMetadata.GetString (invalidFixtureMetadata.GetAssemblyDefinition ().Name), invalidFixtureReader), + ("LookalikeConstructorTypes", lookalikeFixtureReader), }; return Assert.Single (scanner.Scan (assemblies), p => p.ManagedTypeName == managedTypeName); } From b0fb274ec9684d3c0d697698077027d7ed9de870 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:52:13 +0200 Subject: [PATCH 32/45] [typemap] Deduplicate constructor collisions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 4 ++- .../TrimmableTypeMapBuildTests.cs | 28 ++++++++++++++----- .../TrimmableTypeMapGeneratorTests.cs | 3 ++ .../InvalidConstructors.cs | 1 + 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 8f0a953a2be..e199c693ba8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2883,6 +2883,7 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( { var diagnostics = new List (); var signatures = new Dictionary> (StringComparer.Ordinal); + var reportedAmbiguousSignatures = new HashSet (StringComparer.Ordinal); var baseConstructors = CollectBaseRegisteredCtors (typeDef, index); bool hasParameterlessBaseConstructor = baseConstructors.Any (c => c.RegisterInfo.Signature == "()V"); @@ -2927,7 +2928,8 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( } if (signatures.TryGetValue (jniSignature, out var existingManagedSignature) && - !existingManagedSignature.SequenceEqual (signature.ParameterTypes)) { + !existingManagedSignature.SequenceEqual (signature.ParameterTypes) && + reportedAmbiguousSignatures.Add (jniSignature)) { diagnostics.Add (new ConstructorDiagnosticInfo { Kind = ConstructorDiagnosticKind.AmbiguousJniSignature, Detail = jniSignature, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b88dc4dfcef..7bec0029861 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -783,16 +783,24 @@ public UnsupportedExportConstructorOverloads ( AssertNoExportOutputs (builder, "UnsupportedExportConstructorOverloads"); } - [Test] - public void Build_WithCollidingConstructorSignatures_FailsBeforeWritingTrimmableOutputs () + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, false)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, false)] + public void Build_WithCollidingConstructorSignatures_MatchesLegacyCount ( + string typeMapImplementation, + AndroidRuntime runtime, + bool shouldSucceed) { - if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - var proj = new XamarinAndroidApplicationProject (); - proj.SetRuntime (AndroidRuntime.CoreCLR); - proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); proj.Sources.Add (new BuildItem.Source ("ConstructorCollision.cs") { TextContent = () => """ using Android.App; @@ -801,15 +809,21 @@ namespace UnnamedProject; public class ConstructorCollision : Activity { + public enum Kind { None } public ConstructorCollision (int value) { } public ConstructorCollision (uint value) { } + public ConstructorCollision (Kind value) { } } """, }); using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.IsFalse (builder.Build (proj), "Build should fail for constructors that collapse to the same JNI signature."); + Assert.AreEqual (shouldSucceed, builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy collision behavior."); + if (shouldSucceed) { + Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("error XA4259", StringComparison.Ordinal))); + return; + } StringAssertEx.Contains ("error XA4259", builder.LastBuildOutput); var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 629b61bf0bf..a351425ebc3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -468,6 +468,9 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Assert.Contains (logMessages, message => message.StartsWith ("XA4261:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4262:", StringComparison.Ordinal)); Assert.Contains (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); + Assert.Single (logMessages, message => + message.StartsWith ("XA4259:", StringComparison.Ordinal) && + message.Contains ("MyApp.SignedUnsignedCollisionActivity", StringComparison.Ordinal)); foreach (var typeName in new [] { "UnsupportedExportConstructorOverloadsActivity", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 40e6211846a..5ff21dc2e0b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -127,6 +127,7 @@ public class SignedUnsignedCollisionActivity : Android.App.Activity { public SignedUnsignedCollisionActivity (int value) { } public SignedUnsignedCollisionActivity (uint value) { } + public SignedUnsignedCollisionActivity (ConstructorKind value) { } } [Register ("my/app/AliasOne", DoNotGenerateAcw = true)] From 0e30f6563ce0cf7e46952226eaeaf8f4d69c23ea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 17:54:34 +0200 Subject: [PATCH 33/45] [typemap] Tokenize Java super arguments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 268 +++++++++--------- 1 file changed, 137 insertions(+), 131 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index e199c693ba8..dad8eda7a4b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -3050,165 +3050,171 @@ bool IsActivationConstructor (IReadOnlyList parameterTypes) static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) { - bool inString = false; - bool inCharacter = false; - bool inLineComment = false; - bool inBlockComment = false; - bool escaped = false; - var lambdaParameterScopes = new Dictionary (StringComparer.Ordinal); - char precedingSignificantCharacter = '\0'; - char previousSignificantCharacter = '\0'; - - for (int i = 0; i < superArgumentsString.Length; i++) { - char current = superArgumentsString [i]; - char next = i + 1 < superArgumentsString.Length ? superArgumentsString [i + 1] : '\0'; - - if (inLineComment) { - if (current == '\r' || current == '\n') { - inLineComment = false; - } + var tokens = TokenizeJavaExpression (superArgumentsString); + var lambdaDeclarations = new HashSet (); + var lambdaScopes = new List<(string Name, int Start, int End)> (); + for (int i = 0; i < tokens.Count; i++) { + if (tokens [i].Text != "->") { continue; } - if (inBlockComment) { - if (current == '*' && next == '/') { - inBlockComment = false; - i++; - } - continue; + int scopeEnd = FindLambdaBodyEnd (tokens, i + 1, superArgumentsString.Length); + foreach (int declaration in GetLambdaParameterDeclarations (tokens, i)) { + lambdaDeclarations.Add (declaration); + lambdaScopes.Add ((tokens [declaration].Text, tokens [i].End, scopeEnd)); } - if (inString || inCharacter) { - if (escaped) { - escaped = false; - } else if (current == '\\') { - escaped = true; - } else if ((inString && current == '"') || (inCharacter && current == '\'')) { - inString = false; - inCharacter = false; - precedingSignificantCharacter = previousSignificantCharacter; - previousSignificantCharacter = current; - } + } + + for (int i = 0; i < tokens.Count; i++) { + var token = tokens [i]; + if (!token.IsIdentifier || token.Text.Length < 2 || token.Text [0] != 'p' || + token.Text.Skip (1).Any (c => !char.IsDigit (c))) { continue; } - if (current == '/' && next == '/') { - inLineComment = true; - i++; + if (lambdaDeclarations.Contains (i) || + lambdaScopes.Any (scope => scope.Name == token.Text && token.Start >= scope.Start && token.Start < scope.End) || + (i > 0 && tokens [i - 1].Text == ".") || + IsMethodReferenceName (tokens, i)) { continue; } - if (current == '/' && next == '*') { - inBlockComment = true; - i++; - continue; + + var parameterToken = token.Text.Substring (1); + if ((parameterToken.Length > 1 && parameterToken [0] == '0') || + !int.TryParse (parameterToken, out int parameterIndex) || + parameterIndex >= parameterCount) { + return false; } - if (current == '"' || current == '\'') { - inString = current == '"'; - inCharacter = current == '\''; - escaped = false; + } + return true; + } + + readonly record struct JavaLexicalToken (string Text, int Start, int End, bool IsIdentifier); + + static List TokenizeJavaExpression (string value) + { + var tokens = new List (); + for (int i = 0; i < value.Length;) { + if (char.IsWhiteSpace (value [i])) { + i++; continue; } - - if (superArgumentsString [i] != 'p' || i + 1 >= superArgumentsString.Length || - !char.IsDigit (superArgumentsString [i + 1]) || - (i > 0 && IsJavaIdentifierCharacter (superArgumentsString [i - 1])) || - previousSignificantCharacter == '.' || - (precedingSignificantCharacter == ':' && previousSignificantCharacter == ':')) { - if (!char.IsWhiteSpace (current)) { - precedingSignificantCharacter = previousSignificantCharacter; - previousSignificantCharacter = current; + if (i + 1 < value.Length && value [i] == '/' && (value [i + 1] == '/' || value [i + 1] == '*')) { + bool block = value [i + 1] == '*'; + i += 2; + while (i < value.Length && (block ? !(i + 1 < value.Length && value [i] == '*' && value [i + 1] == '/') : value [i] != '\r' && value [i] != '\n')) { + i++; } + i = block && i < value.Length ? Math.Min (i + 2, value.Length) : i; continue; } - - int end = i + 1; - while (end < superArgumentsString.Length && char.IsDigit (superArgumentsString [end])) { - end++; - } - if (end < superArgumentsString.Length && IsJavaIdentifierCharacter (superArgumentsString [end])) { + if (value [i] == '"' || value [i] == '\'') { + char quote = value [i++]; + while (i < value.Length) { + if (value [i] == '\\') { + i = Math.Min (i + 2, value.Length); + } else if (value [i++] == quote) { + break; + } + } continue; } - var parameterName = superArgumentsString.Substring (i, end - i); - if (TryGetLambdaBodyEnd (superArgumentsString, end, out var lambdaBodyEnd)) { - if (!lambdaParameterScopes.TryGetValue (parameterName, out var existingEnd) || lambdaBodyEnd > existingEnd) { - lambdaParameterScopes [parameterName] = lambdaBodyEnd; + if (char.IsLetter (value [i]) || value [i] == '_' || value [i] == '$') { + int start = i++; + while (i < value.Length && IsJavaIdentifierCharacter (value [i])) { + i++; } - precedingSignificantCharacter = previousSignificantCharacter; - previousSignificantCharacter = superArgumentsString [end - 1]; - i = end - 1; - continue; - } else if (lambdaParameterScopes.TryGetValue (parameterName, out var scopeEnd) && i < scopeEnd) { - precedingSignificantCharacter = previousSignificantCharacter; - previousSignificantCharacter = superArgumentsString [end - 1]; - i = end - 1; + tokens.Add (new JavaLexicalToken (value.Substring (start, i - start), start, i, IsIdentifier: true)); continue; } - var parameterToken = superArgumentsString.Substring (i + 1, end - i - 1); - if ((parameterToken.Length > 1 && parameterToken [0] == '0') || - !int.TryParse (parameterToken, out var parameterIndex) || - parameterIndex >= parameterCount) { - return false; - } - precedingSignificantCharacter = previousSignificantCharacter; - previousSignificantCharacter = superArgumentsString [end - 1]; - i = end - 1; + int symbolLength = i + 1 < value.Length && + ((value [i] == '-' && value [i + 1] == '>') || (value [i] == ':' && value [i + 1] == ':')) ? 2 : 1; + tokens.Add (new JavaLexicalToken (value.Substring (i, symbolLength), i, i + symbolLength, IsIdentifier: false)); + i += symbolLength; } - return true; + return tokens; } - static bool TryGetLambdaBodyEnd (string value, int tokenEnd, out int bodyEnd) + static List GetLambdaParameterDeclarations (IReadOnlyList tokens, int arrow) { - int arrow = tokenEnd; - while (arrow < value.Length && char.IsWhiteSpace (value [arrow])) { - arrow++; + if (arrow == 0) { + return []; } - if (arrow + 1 >= value.Length || value [arrow] != '-' || value [arrow + 1] != '>') { - bodyEnd = 0; - return false; + if (tokens [arrow - 1].IsIdentifier) { + return [arrow - 1]; + } + if (tokens [arrow - 1].Text != ")") { + return []; + } + int open = FindMatchingOpenToken (tokens, arrow - 1, "(", ")"); + if (open < 0) { + return []; } - int parentheses = 0; - int brackets = 0; - int braces = 0; - for (int i = arrow + 2; i < value.Length; i++) { - switch (value [i]) { - case '(': - parentheses++; - break; - case '[': - brackets++; - break; - case '{': - braces++; - break; - case ')': - if (parentheses == 0 && brackets == 0 && braces == 0) { - bodyEnd = i; - return true; - } - parentheses--; - break; - case ']': - if (parentheses == 0 && brackets == 0 && braces == 0) { - bodyEnd = i; - return true; - } - brackets--; - break; - case '}': - if (parentheses == 0 && brackets == 0 && braces == 0) { - bodyEnd = i; - return true; - } - braces--; - break; - case ',': - if (parentheses == 0 && brackets == 0 && braces == 0) { - bodyEnd = i; - return true; + var declarations = new List (); + int segmentStart = open + 1; + int angleDepth = 0; + for (int i = segmentStart; i <= arrow - 1; i++) { + string text = tokens [i].Text; + if (text == "<") { + angleDepth++; + } else if (text == ">" && angleDepth > 0) { + angleDepth--; + } + if ((text == "," && angleDepth == 0) || i == arrow - 1) { + int segmentEnd = text == "," ? i : arrow - 1; + for (int j = segmentEnd - 1; j >= segmentStart; j--) { + if (tokens [j].IsIdentifier) { + declarations.Add (j); + break; } - break; + } + segmentStart = i + 1; } } - bodyEnd = value.Length; - return true; + return declarations; + } + + static int FindLambdaBodyEnd (IReadOnlyList tokens, int start, int valueLength) + { + int parentheses = 0, brackets = 0, braces = 0; + for (int i = start; i < tokens.Count; i++) { + string text = tokens [i].Text; + if (text == "," && parentheses == 0 && brackets == 0 && braces == 0) { + return tokens [i].Start; + } + if ((text == ")" && parentheses == 0) || (text == "]" && brackets == 0) || (text == "}" && braces == 0)) { + return tokens [i].Start; + } + parentheses += text == "(" ? 1 : text == ")" ? -1 : 0; + brackets += text == "[" ? 1 : text == "]" ? -1 : 0; + braces += text == "{" ? 1 : text == "}" ? -1 : 0; + } + return valueLength; + } + + static int FindMatchingOpenToken (IReadOnlyList tokens, int close, string openText, string closeText) + { + int depth = 0; + for (int i = close; i >= 0; i--) { + if (tokens [i].Text == closeText) { + depth++; + } else if (tokens [i].Text == openText && --depth == 0) { + return i; + } + } + return -1; + } + + static bool IsMethodReferenceName (IReadOnlyList tokens, int identifier) + { + int previous = identifier - 1; + if (previous >= 0 && tokens [previous].Text == "::") { + return true; + } + if (previous < 0 || tokens [previous].Text != ">") { + return false; + } + int open = FindMatchingOpenToken (tokens, previous, "<", ">"); + return open > 0 && tokens [open - 1].Text == "::"; } static bool IsJavaIdentifierCharacter (char value) => From d29e54c4bd0f75328cd199aea9d80e2e6a6b5285 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 18:02:21 +0200 Subject: [PATCH 34/45] [tests] Cover Java super argument tokens Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapBuildTests.cs | 20 ++++++++- .../InvalidConstructors.cs | 42 +++++++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 6 +++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 7bec0029861..185ce9f0185 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -629,10 +629,20 @@ public class ReservedName : Java.Lang.Object { } } [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "typed-lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "literal-comma", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-parenthesized-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-parenthesized-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-parenthesized-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-reference-bare", "XA4262")] public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( string typeMapImplementation, AndroidRuntime runtime, @@ -646,8 +656,14 @@ public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( var superArguments = shape switch { "lambda" => "p1 -> p1", + "parenthesized-lambda" => "(p1) -> p1", + "typed-lambda" => "(String p1, String p2) -> p1 + p2", + "literal-comma" => "p1 -> \\\"a,b\\\"", "method-reference" => "Helper::p1", + "generic-method-reference" => "Helper::p1", "invalid-bare" => "p1 -> p1, p1", + "invalid-parenthesized-bare" => "(p1) -> p1, p1", + "invalid-generic-reference-bare" => "Helper::p1, p1", _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), }; var proj = new XamarinAndroidApplicationProject { @@ -688,6 +704,8 @@ public SuperArgumentsPeer () { } public class SuperArgumentsBase { public SuperArgumentsBase ( java.util.function.Function function) {} + public SuperArgumentsBase ( + java.util.function.BiFunction function) {} public SuperArgumentsBase ( java.util.function.Supplier supplier) {} public SuperArgumentsBase ( @@ -695,7 +713,7 @@ public SuperArgumentsBase ( Object value) {} public static class Helper { - public static String p1 () { return ""; } + public static T p1 () { return null; } } } """, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 5ff21dc2e0b..a54accdfcbb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -362,6 +362,27 @@ public class LambdaSuperArgumentsActivity : Android.App.Activity public LambdaSuperArgumentsActivity () { } } +[Register ("my/app/ParenthesizedLambdaSuperArgumentsActivity")] +public class ParenthesizedLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1")] + public ParenthesizedLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/TypedLambdaSuperArgumentsActivity")] +public class TypedLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(String p1, String p2) -> p1 + p2")] + public TypedLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/LiteralCommaLambdaSuperArgumentsActivity")] +public class LiteralCommaLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> \"a,b\"")] + public LiteralCommaLambdaSuperArgumentsActivity () { } +} + [Register ("my/app/MethodReferenceSuperArgumentsActivity")] public class MethodReferenceSuperArgumentsActivity : Android.App.Activity { @@ -369,6 +390,13 @@ public class MethodReferenceSuperArgumentsActivity : Android.App.Activity public MethodReferenceSuperArgumentsActivity () { } } +[Register ("my/app/GenericMethodReferenceSuperArgumentsActivity")] +public class GenericMethodReferenceSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1")] + public GenericMethodReferenceSuperArgumentsActivity () { } +} + [Register ("my/app/LambdaWithBareParameterActivity")] public class LambdaWithBareParameterActivity : Android.App.Activity { @@ -376,6 +404,20 @@ public class LambdaWithBareParameterActivity : Android.App.Activity public LambdaWithBareParameterActivity () { } } +[Register ("my/app/ParenthesizedLambdaWithBareParameterActivity")] +public class ParenthesizedLambdaWithBareParameterActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1, p1")] + public ParenthesizedLambdaWithBareParameterActivity () { } +} + +[Register ("my/app/GenericMethodReferenceWithBareParameterActivity")] +public class GenericMethodReferenceWithBareParameterActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1, p1")] + public GenericMethodReferenceWithBareParameterActivity () { } +} + [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index d070eee2cb9..fec70894ee4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -236,6 +236,8 @@ public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed [InlineData ("MyApp.NonCanonicalSuperArgumentZeroActivity", "p00")] [InlineData ("MyApp.NonCanonicalSuperArgumentOneActivity", "p01")] [InlineData ("MyApp.LambdaWithBareParameterActivity", "p1 -> p1, p1")] + [InlineData ("MyApp.ParenthesizedLambdaWithBareParameterActivity", "(p1) -> p1, p1")] + [InlineData ("MyApp.GenericMethodReferenceWithBareParameterActivity", "Helper::p1, p1")] public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) { var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); @@ -257,7 +259,11 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.ValidSuperExpressionActivity")] [InlineData ("MyApp.LexicalSuperArgumentsActivity")] [InlineData ("MyApp.LambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ParenthesizedLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.TypedLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.LiteralCommaLambdaSuperArgumentsActivity")] [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")] + [InlineData ("MyApp.GenericMethodReferenceSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) { Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); From f5173f91b447780d22b7e5e7fe73d8741c52ab4f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 18:08:11 +0200 Subject: [PATCH 35/45] [tests] Complete super argument matrix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 11 +++++++---- .../TrimmableTypeMapBuildTests.cs | 12 ++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index dad8eda7a4b..b86e3579287 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -3039,12 +3039,15 @@ bool IsActivationConstructor (IReadOnlyList parameterTypes) return false; } + bool isJniObjectReference = parameterTypes [0].ManagedTypeName is + "Java.Interop.JniObjectReference" or "Java.Interop.JniObjectReference&"; + var jniObjectReference = parameterTypes [0] with { + ManagedTypeName = "Java.Interop.JniObjectReference", + }; return (IsSpecialManagedType (parameterTypes [0], "System.IntPtr", "System.Runtime", "System.Private.CoreLib", "mscorlib") && IsSpecialManagedType (parameterTypes [1], "Android.Runtime.JniHandleOwnership", "Mono.Android")) || - (IsSpecialManagedType ( - parameterTypes [0] with { ManagedTypeName = parameterTypes [0].ManagedTypeName.TrimEnd ('&') }, - "Java.Interop.JniObjectReference", - "Java.Interop") && + (isJniObjectReference && + IsSpecialManagedType (jniObjectReference, "Java.Interop.JniObjectReference", "Java.Interop") && IsSpecialManagedType (parameterTypes [1], "Java.Interop.JniObjectReferenceOptions", "Java.Interop")); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 185ce9f0185..c2395ac5c33 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -629,11 +629,23 @@ public class ReservedName : Java.Lang.Object { } } [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "lambda", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parenthesized-lambda", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "typed-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "typed-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "typed-lambda", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "literal-comma", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "literal-comma", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "literal-comma", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "method-reference", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-method-reference", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] From a427e672d705a74492ad6022e767ae1c93f243e7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 18:13:00 +0200 Subject: [PATCH 36/45] [tests] Measure explicit constructor metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapBuildTests.cs | 56 +++++++++++++++++++ .../InvalidConstructors.cs | 12 ++-- .../Scanner/ConstructorDetectionTests.cs | 12 ++-- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index c2395ac5c33..4606603e994 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -628,6 +628,62 @@ public class ReservedName : Java.Lang.Object { } } } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "success")] + public void Build_ExplicitExportConstructorAttributeOrders_MatchLegacyPipeline ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ExplicitExportConstructors.cs") { + TextContent = () => """ + using Android.App; + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/RegisterFirst")] + public class RegisterFirst : Activity { + [Register (".ctor", "(I)V", "")] + [Export (".ctor", SuperArgumentsString = "")] + public RegisterFirst (uint value) { } + } + + [Register ("my/app/ExportFirst")] + public class ExportFirst : Activity { + [Export (".ctor", SuperArgumentsString = "")] + [JniConstructorSignature ("(I)V")] + public ExportFirst (uint value) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + var succeeded = builder.Build (proj); + if (expectedCode == "success") { + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should preserve explicit constructor metadata."); + } else { + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should retain measured legacy validation."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + } + } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "lambda", "success")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index a54accdfcbb..1ef23c27744 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -100,19 +100,19 @@ public ExportBeforeRegisterValidActivity (uint value) : base ((int)value) { } } [Register ("my/app/JniBeforeExportValidActivity")] -public class JniBeforeExportValidActivity : IntSignatureBase +public class JniBeforeExportValidActivity : Android.App.Activity { [Java.Interop.JniConstructorSignature ("(I)V")] - [Java.Interop.Export (".ctor")] - public JniBeforeExportValidActivity (uint value) : base ((int)value) { } + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public JniBeforeExportValidActivity (uint value) { } } [Register ("my/app/ExportBeforeJniValidActivity")] -public class ExportBeforeJniValidActivity : IntSignatureBase +public class ExportBeforeJniValidActivity : Android.App.Activity { - [Java.Interop.Export (".ctor")] + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] [Java.Interop.JniConstructorSignature ("(I)V")] - public ExportBeforeJniValidActivity (uint value) : base ((int)value) { } + public ExportBeforeJniValidActivity (uint value) { } } [Register ("my/app/BindingPointerCtor", DoNotGenerateAcw = true)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index fec70894ee4..f5480391382 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -270,14 +270,14 @@ public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTy } [Theory] - [InlineData ("MyApp.RegisterBeforeExportValidActivity")] - [InlineData ("MyApp.ExportBeforeRegisterValidActivity")] - [InlineData ("MyApp.JniBeforeExportValidActivity")] - [InlineData ("MyApp.ExportBeforeJniValidActivity")] - public void ExplicitExportConstructor_PreservesRegistrationAndBaseForwarding (string managedTypeName) + [InlineData ("MyApp.RegisterBeforeExportValidActivity", null)] + [InlineData ("MyApp.ExportBeforeRegisterValidActivity", null)] + [InlineData ("MyApp.JniBeforeExportValidActivity", "")] + [InlineData ("MyApp.ExportBeforeJniValidActivity", "")] + public void ExplicitExportConstructor_PreservesRegistrationAndBaseForwarding (string managedTypeName, string? superArgumentsString) { var constructor = Assert.Single (ScanPeer (managedTypeName).JavaConstructors, c => c.JniSignature == "(I)V"); - Assert.Null (constructor.SuperArgumentsString); + Assert.Equal (superArgumentsString, constructor.SuperArgumentsString); } [Fact] From 34cde66661e225cb6d3bb31d636f3d808e84a720 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 18:30:23 +0200 Subject: [PATCH 37/45] [tests] Mark fixture framework identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index a351425ebc3..cc3ebbbd770 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -374,7 +374,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -416,7 +416,7 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPartialMembers () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); Assert.Equal (10, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4206:", StringComparison.Ordinal))); @@ -459,7 +459,7 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Input ("InvalidConstructorFixtures", invalidReader), ], new Version (11, 0), - new HashSet ()); + TestFixtureFrameworkNames ()); Assert.Empty (result.GeneratedAssemblies); Assert.Empty (result.GeneratedJavaSources); @@ -503,7 +503,7 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -574,7 +574,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -587,7 +587,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -719,6 +719,9 @@ public void Execute_DoesNotSkipJavaPeer_WhenBaseTypeIsTypeForwarded () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + static HashSet TestFixtureFrameworkNames () => + new (StringComparer.OrdinalIgnoreCase) { "TestFixtures" }; + static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); public enum StaleReferenceShape { From da6d39f471eb4e24589581eebc841f055cfe9a80 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 19:28:34 +0200 Subject: [PATCH 38/45] [typemap] Match base constructors by JNI signature Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 8 ++-- .../TrimmableTypeMapBuildTests.cs | 45 +++++++++++++++++++ .../InvalidConstructors.cs | 6 +++ .../Scanner/ConstructorDetectionTests.cs | 1 + 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index b86e3579287..fd0d389b974 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2944,10 +2944,10 @@ IReadOnlyList AnalyzeConstructorDiagnostics ( } string forwardedBaseSignature = exportInfo?.SuperArgumentsString == "" ? "()V" : jniSignature; - bool hasCompatibleBaseConstructor = hasExplicitRegistration - ? baseConstructors.Any (baseCtor => baseCtor.RegisterInfo.Signature == forwardedBaseSignature) - : baseConstructors.Any (baseCtor => - HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); + bool hasCompatibleBaseConstructor = baseConstructors.Any (baseCtor => + baseCtor.RegisterInfo.Signature is string baseJniSignature + ? baseJniSignature == forwardedBaseSignature + : HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); bool canUseParameterlessBaseConstructor = !hasExplicitRegistration && hasParameterlessBaseConstructor; if (baseConstructors.Count > 0 && !hasCompatibleBaseConstructor && !canUseParameterlessBaseConstructor) { diagnostics.Add (new ConstructorDiagnosticInfo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 4606603e994..2861ef0c138 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -684,6 +684,51 @@ public ExportFirst (uint value) { } } } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void Build_ImplicitConstructorUsesCompatibleBaseJniSignature (string typeMapImplementation, AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("CompatibleBaseConstructor.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("my/app/IntBase", DoNotGenerateAcw = true)] + public class IntBase : Java.Lang.Object { + [Register (".ctor", "(I)V", "")] + public IntBase (int value) { } + } + + public class UIntDerived : IntBase { + public UIntDerived (uint value) : base ((int)value) { } + } + """, + }); + proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("my\\app\\IntBase.java") { + Encoding = Encoding.ASCII, + TextContent = () => """ + package my.app; + + public class IntBase { + public IntBase (int value) {} + } + """, + }); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy JNI base compatibility."); + } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "lambda", "success")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 1ef23c27744..f2ad8200d87 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -83,6 +83,12 @@ public class JniSignatureCompatibleBaseActivity : IntSignatureBase public JniSignatureCompatibleBaseActivity (uint value) : base ((int)value) { } } +[Register ("my/app/ImplicitJniCompatibleBaseActivity")] +public class ImplicitJniCompatibleBaseActivity : IntSignatureBase +{ + public ImplicitJniCompatibleBaseActivity (uint value) : base ((int)value) { } +} + [Register ("my/app/RegisterBeforeExportValidActivity")] public class RegisterBeforeExportValidActivity : IntSignatureBase { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index f5480391382..807d44964eb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -250,6 +250,7 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.ExplicitPointerCtorActivity")] [InlineData ("MyApp.ExplicitRegisterCompatibleBaseActivity")] [InlineData ("MyApp.JniSignatureCompatibleBaseActivity")] + [InlineData ("MyApp.ImplicitJniCompatibleBaseActivity")] [InlineData ("MyApp.RegisterBeforeExportValidActivity")] [InlineData ("MyApp.ExportBeforeRegisterValidActivity")] [InlineData ("MyApp.JniBeforeExportValidActivity")] From 6dd076f11e405cba051fb9b0894162c21af0a3a6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 19:30:08 +0200 Subject: [PATCH 39/45] [typemap] Resolve activation constructor identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 78 ++++++------------- .../Generator/FixtureTestBase.cs | 6 +- .../TypeMapAssemblyGeneratorTests.cs | 19 +++++ .../InvalidConstructors.cs | 6 +- .../Scanner/ConstructorDetectionTests.cs | 24 ++++-- 5 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fd0d389b974..9c22de5eece 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2335,7 +2335,7 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para return null; } - static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) + ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) { foreach (var methodHandle in typeDef.GetMethods ()) { var method = index.Reader.GetMethodDefinition (methodHandle); @@ -2344,59 +2344,16 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para continue; } - var signature = index.Reader.GetBlobReader (method.Signature); - var header = signature.ReadSignatureHeader (); - if (header.IsGeneric) { - signature.ReadCompressedInteger (); - } - if (signature.ReadCompressedInteger () != 2 || - (SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.Void) { - continue; - } - - // XI style: (IntPtr, JniHandleOwnership) - var firstParameter = signature; - if ((SignatureTypeCode) signature.ReadByte () == SignatureTypeCode.IntPtr && - IsSignatureType (ref signature, index, "Android.Runtime", "JniHandleOwnership")) { - return ActivationCtorStyle.XamarinAndroid; - } - - // JI style: (ref JniObjectReference, JniObjectReferenceOptions) - signature = firstParameter; - if ((SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.ByReference) { - signature = firstParameter; - } - if (IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReference") && - IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReferenceOptions")) { - return ActivationCtorStyle.JavaInterop; + var sig = method.DecodeSignature (index.TypeRefSignatureProvider, index); + var style = GetActivationConstructorStyle (sig.ParameterTypes); + if (style is not null) { + return style; } } return null; } - static bool IsSignatureType (ref BlobReader signature, AssemblyIndex index, string typeNamespace, string typeName) - { - var kind = (SignatureTypeKind) signature.ReadByte (); - if (kind is not (SignatureTypeKind.Class or SignatureTypeKind.ValueType)) { - return false; - } - - var handle = signature.ReadTypeHandle (); - switch (handle.Kind) { - case HandleKind.TypeReference: - var typeRef = index.Reader.GetTypeReference ((TypeReferenceHandle) handle); - return index.Reader.StringComparer.Equals (typeRef.Namespace, typeNamespace) && - index.Reader.StringComparer.Equals (typeRef.Name, typeName); - case HandleKind.TypeDefinition: - var typeDef = index.Reader.GetTypeDefinition ((TypeDefinitionHandle) handle); - return index.Reader.StringComparer.Equals (typeDef.Namespace, typeNamespace) && - index.Reader.StringComparer.Equals (typeDef.Name, typeName); - default: - return false; - } - } - /// /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying /// type's (fullName, assemblyName) by reading the raw signature blob. @@ -3034,9 +2991,12 @@ static bool TryValidateConstructorParameterShapes ( } bool IsActivationConstructor (IReadOnlyList parameterTypes) + => GetActivationConstructorStyle (parameterTypes) is not null; + + ActivationCtorStyle? GetActivationConstructorStyle (IReadOnlyList parameterTypes) { if (parameterTypes.Count != 2) { - return false; + return null; } bool isJniObjectReference = parameterTypes [0].ManagedTypeName is @@ -3044,13 +3004,23 @@ bool IsActivationConstructor (IReadOnlyList parameterTypes) var jniObjectReference = parameterTypes [0] with { ManagedTypeName = "Java.Interop.JniObjectReference", }; - return (IsSpecialManagedType (parameterTypes [0], "System.IntPtr", "System.Runtime", "System.Private.CoreLib", "mscorlib") && - IsSpecialManagedType (parameterTypes [1], "Android.Runtime.JniHandleOwnership", "Mono.Android")) || - (isJniObjectReference && - IsSpecialManagedType (jniObjectReference, "Java.Interop.JniObjectReference", "Java.Interop") && - IsSpecialManagedType (parameterTypes [1], "Java.Interop.JniObjectReferenceOptions", "Java.Interop")); + if (IsSpecialManagedType (parameterTypes [0], "System.IntPtr", "System.Runtime", "System.Private.CoreLib", "mscorlib") && + IsActivationFrameworkType (parameterTypes [1], "Android.Runtime.JniHandleOwnership", "Mono.Android")) { + return ActivationCtorStyle.XamarinAndroid; + } + if (isJniObjectReference && + IsActivationFrameworkType (jniObjectReference, "Java.Interop.JniObjectReference", "Java.Interop") && + IsActivationFrameworkType (parameterTypes [1], "Java.Interop.JniObjectReferenceOptions", "Java.Interop")) { + return ActivationCtorStyle.JavaInterop; + } + return null; } + bool IsActivationFrameworkType (TypeRefData type, string managedTypeName, string assemblyName) => + IsSpecialManagedType (type, managedTypeName, assemblyName) || + (string.Equals (type.ManagedTypeName, managedTypeName, StringComparison.Ordinal) && + frameworkAssemblyNames.Contains (type.AssemblyName)); + static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) { var tokens = TokenizeJavaExpression (superArgumentsString); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index dd12967da2f..5f761cfd81a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -34,11 +34,13 @@ static string TestAttributeFixtureAssemblyPath { } static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { - using var scanner = new JavaPeerScanner (); using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath)); + var testFixtureInput = GetAssemblyInput (peReader); + using var scanner = new JavaPeerScanner (frameworkAssemblyNames: + new HashSet (StringComparer.OrdinalIgnoreCase) { testFixtureInput.Name }); var assemblies = new [] { - GetAssemblyInput (peReader), + testFixtureInput, GetAssemblyInput (attributePeReader), }; var peers = scanner.Scan (assemblies); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 5a5a0c2f4f8..55fe60952c8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -187,6 +187,25 @@ public void Generate_InheritedCtor_CreateInstanceDoesNotActivate () AssertCreateInstanceReturnsNull (pe, reader, "MyApp_SimpleActivity_Proxy"); } + [Fact] + public void Generate_NoActivationCtor_CreateInstanceDoesNotReferenceLookalikeSignature () + { + var peer = MakeMcwPeer ("test/Lookalike", "Test.Lookalike", "TestAsm") with { + DoNotGenerateAcw = true, + }; + using var stream = GenerateAssembly ([peer], "LookalikeCreateInstanceTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + AssertCreateInstanceReturnsNull (pe, reader, "Test_Lookalike_Proxy"); + Assert.Empty (FindCtorMemberRefs ( + reader, + "Test", + "Lookalike", + "System.IntPtr", + "Android.Runtime.JniHandleOwnership")); + } + [Fact] public void Generate_InheritedJavaInteropCtor_CreateInstanceDoesNotActivate () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index f2ad8200d87..66515af1b1f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -178,19 +178,19 @@ public CrossAssemblyBorrowActivity (CrossAssemblyBorrowedParameter value) { } } [Register ("my/app/PrimitiveLookalikeActivity")] -public class PrimitiveLookalikeActivity : Android.App.Activity +public class PrimitiveLookalikeActivity { public PrimitiveLookalikeActivity (LookalikeInt32 value) { } } [Register ("my/app/XamarinActivationLookalikeActivity")] -public class XamarinActivationLookalikeActivity : Android.App.Activity +public class XamarinActivationLookalikeActivity { public XamarinActivationLookalikeActivity (LookalikeIntPtr handle, LookalikeOwnership ownership) { } } [Register ("my/app/JniActivationLookalikeActivity")] -public class JniActivationLookalikeActivity : Android.App.Activity +public class JniActivationLookalikeActivity { public JniActivationLookalikeActivity (LookalikeJniObjectReference reference, LookalikeJniObjectReferenceOptions options) { } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 807d44964eb..556bd48367a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -202,14 +202,25 @@ public void ConstructorDescriptor_DoesNotBorrowSameNamedTypeFromAnotherAssembly } [Theory] - [InlineData ("MyApp.PrimitiveLookalikeActivity", "(Lmy/app/LookalikeInt32;)V")] - [InlineData ("MyApp.XamarinActivationLookalikeActivity", "(Lmy/app/LookalikeIntPtr;Lmy/app/LookalikeOwnership;)V")] - [InlineData ("MyApp.JniActivationLookalikeActivity", "(Lmy/app/LookalikeReference;Lmy/app/LookalikeOptions;)V")] - public void PrimitiveAndActivationLookalikes_UseRegisteredJavaDescriptors (string managedTypeName, string jniSignature) + [InlineData ("MyApp.PrimitiveLookalikeActivity")] + [InlineData ("MyApp.XamarinActivationLookalikeActivity")] + [InlineData ("MyApp.JniActivationLookalikeActivity")] + public void PrimitiveAndActivationLookalikes_UseRegisteredJavaDescriptors (string managedTypeName) { var peer = ScanPeer (managedTypeName); Assert.Empty (peer.ConstructorDiagnostics); - Assert.Contains (peer.JavaConstructors, constructor => constructor.JniSignature == jniSignature); + Assert.Null (peer.ActivationCtor); + } + + [Theory] + [InlineData ("my/app/GlobalType", ActivationCtorStyle.XamarinAndroid)] + [InlineData ("my/app/JiStylePeer", ActivationCtorStyle.JavaInterop)] + public void CanonicalActivationConstructors_AreRecognized (string javaName, ActivationCtorStyle style) + { + var peer = FindFixtureByJavaName (javaName); + Assert.NotNull (peer.ActivationCtor); + Assert.Equal (style, peer.ActivationCtor.Style); + Assert.Equal (peer.ManagedTypeName, peer.ActivationCtor.DeclaringTypeName); } [Fact] @@ -306,7 +317,8 @@ static System.Collections.Generic.IReadOnlyList ScanC static JavaPeerInfo ScanPeer (string managedTypeName) { - using var scanner = new JavaPeerScanner (); + using var scanner = new JavaPeerScanner (frameworkAssemblyNames: + new System.Collections.Generic.HashSet (StringComparer.OrdinalIgnoreCase) { "TestFixtures" }); var testAssemblyDir = Path.GetDirectoryName (typeof (ConstructorDetectionTests).Assembly.Location) ?? throw new InvalidOperationException ("Cannot determine test assembly directory."); using var fixtureReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); From 4676e335d61464041b439a898927da08bc675f0e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 19:44:38 +0200 Subject: [PATCH 40/45] [typemap] Keep generic lambda bodies scoped Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 30 +++++++++++++++++-- .../TrimmableTypeMapBuildTests.cs | 18 ++++++++++- .../InvalidConstructors.cs | 21 +++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 3 ++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 9c22de5eece..f77f2834edd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -3148,13 +3148,19 @@ static List GetLambdaParameterDeclarations (IReadOnlyList static int FindLambdaBodyEnd (IReadOnlyList tokens, int start, int valueLength) { - int parentheses = 0, brackets = 0, braces = 0; + int parentheses = 0, brackets = 0, braces = 0, genericArguments = 0; for (int i = start; i < tokens.Count; i++) { string text = tokens [i].Text; - if (text == "," && parentheses == 0 && brackets == 0 && braces == 0) { + if (text == "<" && (genericArguments > 0 || IsGenericArgumentOpen (tokens, i))) { + genericArguments++; + } else if (text == ">" && genericArguments > 0) { + genericArguments--; + } + if (text == "," && parentheses == 0 && brackets == 0 && braces == 0 && genericArguments == 0) { return tokens [i].Start; } - if ((text == ")" && parentheses == 0) || (text == "]" && brackets == 0) || (text == "}" && braces == 0)) { + if (genericArguments == 0 && + ((text == ")" && parentheses == 0) || (text == "]" && brackets == 0) || (text == "}" && braces == 0))) { return tokens [i].Start; } parentheses += text == "(" ? 1 : text == ")" ? -1 : 0; @@ -3164,6 +3170,24 @@ static int FindLambdaBodyEnd (IReadOnlyList tokens, int start, return valueLength; } + static bool IsGenericArgumentOpen (IReadOnlyList tokens, int open) + { + int depth = 0; + for (int i = open; i < tokens.Count; i++) { + if (tokens [i].Text == "<") { + depth++; + } else if (tokens [i].Text == ">" && --depth == 0) { + if (i + 1 >= tokens.Count) { + return false; + } + string following = tokens [i + 1].Text; + return following is "(" or "[" or "." or "::" || + (tokens [i + 1].IsIdentifier && open > 0 && tokens [open - 1].Text is "." or "::"); + } + } + return false; + } + static int FindMatchingOpenToken (IReadOnlyList tokens, int close, string openText, string closeText) { int depth = 0; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 2861ef0c138..ebc2ddcc3f9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -747,6 +747,12 @@ public IntBase (int value) {} [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-method-reference", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-construction", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-construction", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-construction", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "nested-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "nested-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "nested-generic", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] @@ -756,6 +762,9 @@ public IntBase (int value) {} [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-reference-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-construction-bare", "XA4262")] public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( string typeMapImplementation, AndroidRuntime runtime, @@ -774,9 +783,12 @@ public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( "literal-comma" => "p1 -> \\\"a,b\\\"", "method-reference" => "Helper::p1", "generic-method-reference" => "Helper::p1", + "generic-construction" => "(p1) -> new SimpleEntry(p1, p1)", + "nested-generic" => "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\\\"a,b\\\", p1))", "invalid-bare" => "p1 -> p1, p1", "invalid-parenthesized-bare" => "(p1) -> p1, p1", "invalid-generic-reference-bare" => "Helper::p1, p1", + "invalid-generic-construction-bare" => "(p1) -> new SimpleEntry(p1, p1), p1", _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), }; var proj = new XamarinAndroidApplicationProject { @@ -816,7 +828,7 @@ public SuperArgumentsPeer () { } public class SuperArgumentsBase { public SuperArgumentsBase ( - java.util.function.Function function) {} + java.util.function.Function function) {} public SuperArgumentsBase ( java.util.function.BiFunction function) {} public SuperArgumentsBase ( @@ -828,6 +840,10 @@ public SuperArgumentsBase ( public static class Helper { public static T p1 () { return null; } } + + public static class SimpleEntry extends java.util.AbstractMap.SimpleEntry { + public SimpleEntry (K key, V value) { super (key, value); } + } } """, }); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index 66515af1b1f..e59ffff68f6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -389,6 +389,20 @@ public class LiteralCommaLambdaSuperArgumentsActivity : Android.App.Activity public LiteralCommaLambdaSuperArgumentsActivity () { } } +[Register ("my/app/GenericConstructionLambdaSuperArgumentsActivity")] +public class GenericConstructionLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry(p1, p1)")] + public GenericConstructionLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/NestedGenericLambdaSuperArgumentsActivity")] +public class NestedGenericLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\"a,b\", p1))")] + public NestedGenericLambdaSuperArgumentsActivity () { } +} + [Register ("my/app/MethodReferenceSuperArgumentsActivity")] public class MethodReferenceSuperArgumentsActivity : Android.App.Activity { @@ -424,6 +438,13 @@ public class GenericMethodReferenceWithBareParameterActivity : Android.App.Activ public GenericMethodReferenceWithBareParameterActivity () { } } +[Register ("my/app/GenericConstructionWithBareParameterActivity")] +public class GenericConstructionWithBareParameterActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry(p1, p1), p1")] + public GenericConstructionWithBareParameterActivity () { } +} + [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 556bd48367a..981f993903e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -249,6 +249,7 @@ public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed [InlineData ("MyApp.LambdaWithBareParameterActivity", "p1 -> p1, p1")] [InlineData ("MyApp.ParenthesizedLambdaWithBareParameterActivity", "(p1) -> p1, p1")] [InlineData ("MyApp.GenericMethodReferenceWithBareParameterActivity", "Helper::p1, p1")] + [InlineData ("MyApp.GenericConstructionWithBareParameterActivity", "(p1) -> new SimpleEntry(p1, p1), p1")] public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) { var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); @@ -274,6 +275,8 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.ParenthesizedLambdaSuperArgumentsActivity")] [InlineData ("MyApp.TypedLambdaSuperArgumentsActivity")] [InlineData ("MyApp.LiteralCommaLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.GenericConstructionLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.NestedGenericLambdaSuperArgumentsActivity")] [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")] [InlineData ("MyApp.GenericMethodReferenceSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) From 1ffbf5044823c4bbba079671899c3daa36bbaed7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 19:44:46 +0200 Subject: [PATCH 41/45] [tests] Check complete legacy constructor set Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ConstructorParityTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs index e46e6066630..9c736d365dc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs @@ -28,7 +28,8 @@ public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) var fixturePath = UserTypesFixturePath; Assert.NotNull (fixturePath); var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName); - Assert.Single (constructors, c => c.JniSignature == "()V"); + var constructor = Assert.Single (constructors); + Assert.Equal ("()V", constructor.JniSignature); } [Theory] From dffd0332edf4727f720eea511b69bf669fbd817e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 19:46:07 +0200 Subject: [PATCH 42/45] [tests] Use canonical activation assemblies Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ActivationStubs.Java.Interop.csproj | 9 ++++++++ .../JniObjectReference.cs | 15 +++++++++++++ .../ActivationStubs.Mono.Android.csproj | 9 ++++++++ .../JniHandleOwnership.cs | 8 +++++++ .../Generator/FixtureTestBase.cs | 6 ++---- .../TrimmableTypeMapGeneratorTests.cs | 15 ++++++------- ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 4 ++++ .../Scanner/ConstructorDetectionTests.cs | 3 +-- .../TestFixtures/StubAttributes.cs | 21 ------------------- .../TestFixtures/TestFixtures.csproj | 2 ++ 10 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj new file mode 100644 index 00000000000..63d9e010ce6 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj @@ -0,0 +1,9 @@ + + + + $(DotNetStableTargetFramework) + Java.Interop + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs new file mode 100644 index 00000000000..da787fca570 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs @@ -0,0 +1,15 @@ +using System; + +namespace Java.Interop; + +public struct JniObjectReference +{ + public IntPtr Handle; +} + +public enum JniObjectReferenceOptions +{ + None, + Copy, + CopyAndDispose, +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj new file mode 100644 index 00000000000..fb574c524e6 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj @@ -0,0 +1,9 @@ + + + + $(DotNetStableTargetFramework) + Mono.Android + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs new file mode 100644 index 00000000000..115d615d2c4 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs @@ -0,0 +1,8 @@ +namespace Android.Runtime; + +public enum JniHandleOwnership +{ + DoNotTransfer, + TransferLocalRef, + TransferGlobalRef, +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 5f761cfd81a..5ac0e10eb09 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -36,11 +36,9 @@ static string TestAttributeFixtureAssemblyPath { static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath)); - var testFixtureInput = GetAssemblyInput (peReader); - using var scanner = new JavaPeerScanner (frameworkAssemblyNames: - new HashSet (StringComparer.OrdinalIgnoreCase) { testFixtureInput.Name }); + using var scanner = new JavaPeerScanner (); var assemblies = new [] { - testFixtureInput, + GetAssemblyInput (peReader), GetAssemblyInput (attributePeReader), }; var peers = scanner.Scan (assemblies); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index cc3ebbbd770..a351425ebc3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -374,7 +374,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); + 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"); @@ -416,7 +416,7 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPartialMembers () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); Assert.Equal (10, logMessages.Count (message => message.StartsWith ("XA4263:", StringComparison.Ordinal))); Assert.Equal (1, logMessages.Count (message => message.StartsWith ("XA4206:", StringComparison.Ordinal))); @@ -459,7 +459,7 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () Input ("InvalidConstructorFixtures", invalidReader), ], new Version (11, 0), - TestFixtureFrameworkNames ()); + new HashSet ()); Assert.Empty (result.GeneratedAssemblies); Assert.Empty (result.GeneratedJavaSources); @@ -503,7 +503,7 @@ public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); + 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 @@ -574,7 +574,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); + 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); @@ -587,7 +587,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), TestFixtureFrameworkNames ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -719,9 +719,6 @@ public void Execute_DoesNotSkipJavaPeer_WhenBaseTypeIsTypeForwarded () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); - static HashSet TestFixtureFrameworkNames () => - new (StringComparer.OrdinalIgnoreCase) { "TestFixtures" }; - static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); public enum StaleReferenceShape { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 9c91a0ef378..ff9cadf61f2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -16,6 +16,8 @@ + + @@ -48,6 +50,8 @@ <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" /> <_TestFixtureFiles Include="InvalidConstructorFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\InvalidConstructorFixtures.dll" /> <_TestFixtureFiles Include="LookalikeConstructorTypes\bin\$(Configuration)\$(DotNetStableTargetFramework)\LookalikeConstructorTypes.dll" /> + <_TestFixtureFiles Include="ActivationStubs.Java.Interop\bin\$(Configuration)\$(DotNetStableTargetFramework)\Java.Interop.dll" /> + <_TestFixtureFiles Include="ActivationStubs.Mono.Android\bin\$(Configuration)\$(DotNetStableTargetFramework)\Mono.Android.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 981f993903e..cdc5a467086 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -320,8 +320,7 @@ static System.Collections.Generic.IReadOnlyList ScanC static JavaPeerInfo ScanPeer (string managedTypeName) { - using var scanner = new JavaPeerScanner (frameworkAssemblyNames: - new System.Collections.Generic.HashSet (StringComparer.OrdinalIgnoreCase) { "TestFixtures" }); + using var scanner = new JavaPeerScanner (); var testAssemblyDir = Path.GetDirectoryName (typeof (ConstructorDetectionTests).Assembly.Location) ?? throw new InvalidOperationException ("Cannot determine test assembly directory."); using var fixtureReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 2baf133e290..c8ae803f862 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -1,15 +1,5 @@ using System; -namespace Android.Runtime -{ - public enum JniHandleOwnership - { - DoNotTransfer = 0, - TransferLocalRef = 1, - TransferGlobalRef = 2, - } -} - namespace Lookalike { [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] @@ -33,17 +23,6 @@ public ExportFieldAttribute (string name) { } namespace Java.Interop { - public struct JniObjectReference - { - public IntPtr Handle; - } - - public enum JniObjectReferenceOptions - { - None = 0, - Copy = 1, - CopyAndDispose = 2, - } } namespace Android.App diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj index c5f61f8238c..7b81de1019f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -12,6 +12,8 @@ + + From febf769a2f0aa2c9b8fbf7321900e4115fd6fbef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 20:09:51 +0200 Subject: [PATCH 43/45] [tests] Cover generic lambda delimiters Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapBuildTests.cs | 28 ++++++++++++++----- .../InvalidConstructors.cs | 21 ++++++++++++++ .../Scanner/ConstructorDetectionTests.cs | 3 ++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index ebc2ddcc3f9..3a617fda80d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -753,6 +753,12 @@ public IntBase (int value) {} [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "nested-generic", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "comparison", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "comparison", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "comparison", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "shift", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "shift", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "shift", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] @@ -765,6 +771,9 @@ public IntBase (int value) {} [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-construction-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-comparison-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-comparison-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-comparison-bare", "XA4262")] public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( string typeMapImplementation, AndroidRuntime runtime, @@ -785,12 +794,21 @@ public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( "generic-method-reference" => "Helper::p1", "generic-construction" => "(p1) -> new SimpleEntry(p1, p1)", "nested-generic" => "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\\\"a,b\\\", p1))", + "comparison" => "(int p1) -> p1 < 2 ? p1 : 2", + "shift" => "(int p1) -> p1 >> 1", "invalid-bare" => "p1 -> p1, p1", "invalid-parenthesized-bare" => "(p1) -> p1, p1", "invalid-generic-reference-bare" => "Helper::p1, p1", "invalid-generic-construction-bare" => "(p1) -> new SimpleEntry(p1, p1), p1", + "invalid-comparison-bare" => "(int p1) -> p1 < 2 ? p1 : 2, p1", _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), }; + var functionalType = shape switch { + "typed-lambda" => "java.util.function.BiFunction", + "comparison" or "shift" or "invalid-comparison-bare" => "java.util.function.IntUnaryOperator", + "method-reference" or "generic-method-reference" or "invalid-generic-reference-bare" => "java.util.function.Supplier", + _ => "java.util.function.Function", + }; var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, References = { @@ -823,18 +841,14 @@ public SuperArgumentsPeer () { } }); proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("my\\app\\SuperArgumentsBase.java") { Encoding = Encoding.ASCII, - TextContent = () => """ + TextContent = () => $$""" package my.app; public class SuperArgumentsBase { public SuperArgumentsBase ( - java.util.function.Function function) {} - public SuperArgumentsBase ( - java.util.function.BiFunction function) {} - public SuperArgumentsBase ( - java.util.function.Supplier supplier) {} + {{functionalType}} function) {} public SuperArgumentsBase ( - java.util.function.Function function, + java.util.function.Function function, Object value) {} public static class Helper { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index e59ffff68f6..bca18e29eb1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -403,6 +403,20 @@ public class NestedGenericLambdaSuperArgumentsActivity : Android.App.Activity public NestedGenericLambdaSuperArgumentsActivity () { } } +[Register ("my/app/ComparisonLambdaSuperArgumentsActivity")] +public class ComparisonLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 < 2 ? p1 : 2")] + public ComparisonLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/ShiftLambdaSuperArgumentsActivity")] +public class ShiftLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 >> 1")] + public ShiftLambdaSuperArgumentsActivity () { } +} + [Register ("my/app/MethodReferenceSuperArgumentsActivity")] public class MethodReferenceSuperArgumentsActivity : Android.App.Activity { @@ -445,6 +459,13 @@ public class GenericConstructionWithBareParameterActivity : Android.App.Activity public GenericConstructionWithBareParameterActivity () { } } +[Register ("my/app/ComparisonWithBareParameterActivity")] +public class ComparisonWithBareParameterActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 < 2 ? p1 : 2, p1")] + public ComparisonWithBareParameterActivity () { } +} + [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index cdc5a467086..cf90158574d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -250,6 +250,7 @@ public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed [InlineData ("MyApp.ParenthesizedLambdaWithBareParameterActivity", "(p1) -> p1, p1")] [InlineData ("MyApp.GenericMethodReferenceWithBareParameterActivity", "Helper::p1, p1")] [InlineData ("MyApp.GenericConstructionWithBareParameterActivity", "(p1) -> new SimpleEntry(p1, p1), p1")] + [InlineData ("MyApp.ComparisonWithBareParameterActivity", "(int p1) -> p1 < 2 ? p1 : 2, p1")] public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) { var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); @@ -277,6 +278,8 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.LiteralCommaLambdaSuperArgumentsActivity")] [InlineData ("MyApp.GenericConstructionLambdaSuperArgumentsActivity")] [InlineData ("MyApp.NestedGenericLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ComparisonLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ShiftLambdaSuperArgumentsActivity")] [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")] [InlineData ("MyApp.GenericMethodReferenceSuperArgumentsActivity")] public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) From 48e2acb2dc42a4d7f4d03bb5b09ff5d2b2f03286 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 20:22:49 +0200 Subject: [PATCH 44/45] [tests] Record element-mapped legacy constructors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ConstructorParityTests.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs index 9c736d365dc..c36340da8e9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs @@ -19,8 +19,6 @@ public void LegacyConstructorSignatureCollision_KeepsFirstSignature (string mana [Theory] [InlineData ("UserApp.GenericParameterConstructor`1")] [InlineData ("UserApp.GenericInstantiationConstructor")] - [InlineData ("UserApp.ByRefConstructor")] - [InlineData ("UserApp.PointerConstructor")] [InlineData ("UserApp.FunctionPointerConstructor")] [InlineData ("UserApp.FunctionPointerArrayConstructor")] public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) @@ -32,6 +30,19 @@ public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) Assert.Equal ("()V", constructor.JniSignature); } + [Theory] + [InlineData ("UserApp.ByRefConstructor")] + [InlineData ("UserApp.PointerConstructor")] + public void LegacyElementMappedConstructor_UsesMeasuredJniSignatures (string managedTypeName) + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var signatures = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName) + .Select (constructor => constructor.JniSignature) + .ToList (); + Assert.Equal (["()V", "(I)V"], signatures); + } + [Theory] [InlineData ("UserApp.RectangularArrayConstructor", "([Ljava/lang/String;)V")] [InlineData ("UserApp.NestedRectangularArrayConstructor", "([Ljava/lang/String;)V")] From 663a4abd972df8748dc1089df05a21892e077792 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 20:56:25 +0200 Subject: [PATCH 45/45] [typemap] Defer complex super arguments to javac Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 132 +----------------- .../TrimmableTypeMapBuildTests.cs | 41 +++--- .../InvalidConstructors.cs | 42 +----- .../Scanner/ConstructorDetectionTests.cs | 6 +- 4 files changed, 33 insertions(+), 188 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f77f2834edd..a69e2839ba7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -3024,17 +3024,8 @@ bool IsActivationFrameworkType (TypeRefData type, string managedTypeName, string static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) { var tokens = TokenizeJavaExpression (superArgumentsString); - var lambdaDeclarations = new HashSet (); - var lambdaScopes = new List<(string Name, int Start, int End)> (); - for (int i = 0; i < tokens.Count; i++) { - if (tokens [i].Text != "->") { - continue; - } - int scopeEnd = FindLambdaBodyEnd (tokens, i + 1, superArgumentsString.Length); - foreach (int declaration in GetLambdaParameterDeclarations (tokens, i)) { - lambdaDeclarations.Add (declaration); - lambdaScopes.Add ((tokens [declaration].Text, tokens [i].End, scopeEnd)); - } + if (tokens.Any (token => token.Text is "->" or "::")) { + return true; } for (int i = 0; i < tokens.Count; i++) { @@ -3043,10 +3034,7 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa token.Text.Skip (1).Any (c => !char.IsDigit (c))) { continue; } - if (lambdaDeclarations.Contains (i) || - lambdaScopes.Any (scope => scope.Name == token.Text && token.Start >= scope.Start && token.Start < scope.End) || - (i > 0 && tokens [i - 1].Text == ".") || - IsMethodReferenceName (tokens, i)) { + if (i > 0 && tokens [i - 1].Text == ".") { continue; } @@ -3060,7 +3048,7 @@ static bool HasValidSuperArgumentReferences (string superArgumentsString, int pa return true; } - readonly record struct JavaLexicalToken (string Text, int Start, int End, bool IsIdentifier); + readonly record struct JavaLexicalToken (string Text, bool IsIdentifier); static List TokenizeJavaExpression (string value) { @@ -3095,125 +3083,17 @@ static List TokenizeJavaExpression (string value) while (i < value.Length && IsJavaIdentifierCharacter (value [i])) { i++; } - tokens.Add (new JavaLexicalToken (value.Substring (start, i - start), start, i, IsIdentifier: true)); + tokens.Add (new JavaLexicalToken (value.Substring (start, i - start), IsIdentifier: true)); continue; } int symbolLength = i + 1 < value.Length && ((value [i] == '-' && value [i + 1] == '>') || (value [i] == ':' && value [i + 1] == ':')) ? 2 : 1; - tokens.Add (new JavaLexicalToken (value.Substring (i, symbolLength), i, i + symbolLength, IsIdentifier: false)); + tokens.Add (new JavaLexicalToken (value.Substring (i, symbolLength), IsIdentifier: false)); i += symbolLength; } return tokens; } - static List GetLambdaParameterDeclarations (IReadOnlyList tokens, int arrow) - { - if (arrow == 0) { - return []; - } - if (tokens [arrow - 1].IsIdentifier) { - return [arrow - 1]; - } - if (tokens [arrow - 1].Text != ")") { - return []; - } - int open = FindMatchingOpenToken (tokens, arrow - 1, "(", ")"); - if (open < 0) { - return []; - } - - var declarations = new List (); - int segmentStart = open + 1; - int angleDepth = 0; - for (int i = segmentStart; i <= arrow - 1; i++) { - string text = tokens [i].Text; - if (text == "<") { - angleDepth++; - } else if (text == ">" && angleDepth > 0) { - angleDepth--; - } - if ((text == "," && angleDepth == 0) || i == arrow - 1) { - int segmentEnd = text == "," ? i : arrow - 1; - for (int j = segmentEnd - 1; j >= segmentStart; j--) { - if (tokens [j].IsIdentifier) { - declarations.Add (j); - break; - } - } - segmentStart = i + 1; - } - } - return declarations; - } - - static int FindLambdaBodyEnd (IReadOnlyList tokens, int start, int valueLength) - { - int parentheses = 0, brackets = 0, braces = 0, genericArguments = 0; - for (int i = start; i < tokens.Count; i++) { - string text = tokens [i].Text; - if (text == "<" && (genericArguments > 0 || IsGenericArgumentOpen (tokens, i))) { - genericArguments++; - } else if (text == ">" && genericArguments > 0) { - genericArguments--; - } - if (text == "," && parentheses == 0 && brackets == 0 && braces == 0 && genericArguments == 0) { - return tokens [i].Start; - } - if (genericArguments == 0 && - ((text == ")" && parentheses == 0) || (text == "]" && brackets == 0) || (text == "}" && braces == 0))) { - return tokens [i].Start; - } - parentheses += text == "(" ? 1 : text == ")" ? -1 : 0; - brackets += text == "[" ? 1 : text == "]" ? -1 : 0; - braces += text == "{" ? 1 : text == "}" ? -1 : 0; - } - return valueLength; - } - - static bool IsGenericArgumentOpen (IReadOnlyList tokens, int open) - { - int depth = 0; - for (int i = open; i < tokens.Count; i++) { - if (tokens [i].Text == "<") { - depth++; - } else if (tokens [i].Text == ">" && --depth == 0) { - if (i + 1 >= tokens.Count) { - return false; - } - string following = tokens [i + 1].Text; - return following is "(" or "[" or "." or "::" || - (tokens [i + 1].IsIdentifier && open > 0 && tokens [open - 1].Text is "." or "::"); - } - } - return false; - } - - static int FindMatchingOpenToken (IReadOnlyList tokens, int close, string openText, string closeText) - { - int depth = 0; - for (int i = close; i >= 0; i--) { - if (tokens [i].Text == closeText) { - depth++; - } else if (tokens [i].Text == openText && --depth == 0) { - return i; - } - } - return -1; - } - - static bool IsMethodReferenceName (IReadOnlyList tokens, int identifier) - { - int previous = identifier - 1; - if (previous >= 0 && tokens [previous].Text == "::") { - return true; - } - if (previous < 0 || tokens [previous].Text != ">") { - return false; - } - int open = FindMatchingOpenToken (tokens, previous, "<", ">"); - return open > 0 && tokens [open - 1].Text == "::"; - } - static bool IsJavaIdentifierCharacter (char value) => char.IsLetterOrDigit (value) || value == '_' || value == '$'; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 3a617fda80d..f50f51edc38 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -753,27 +753,24 @@ public IntBase (int value) {} [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "nested-generic", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "instanceof-generic", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "comparison", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "comparison", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "comparison", "success")] [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "shift", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "shift", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "shift", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-bare", "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-bare", "XA4262")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-bare", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-parenthesized-bare", "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-parenthesized-bare", "XA4262")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-parenthesized-bare", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-reference-bare", "XA4262")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-reference-bare", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-generic-construction-bare", "XA4262")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-generic-construction-bare", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "invalid-comparison-bare", "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "invalid-comparison-bare", "XA4262")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "invalid-comparison-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-call", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-call", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-call", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-arithmetic", "XA4262")] public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( string typeMapImplementation, AndroidRuntime runtime, @@ -794,19 +791,19 @@ public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( "generic-method-reference" => "Helper::p1", "generic-construction" => "(p1) -> new SimpleEntry(p1, p1)", "nested-generic" => "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\\\"a,b\\\", p1))", + "instanceof-generic" => "(p1) -> p1 instanceof java.util.Map ? p1 : p1", "comparison" => "(int p1) -> p1 < 2 ? p1 : 2", "shift" => "(int p1) -> p1 >> 1", - "invalid-bare" => "p1 -> p1, p1", - "invalid-parenthesized-bare" => "(p1) -> p1, p1", - "invalid-generic-reference-bare" => "Helper::p1, p1", - "invalid-generic-construction-bare" => "(p1) -> new SimpleEntry(p1, p1), p1", - "invalid-comparison-bare" => "(int p1) -> p1 < 2 ? p1 : 2, p1", + "ordinary-bare" => "p1", + "ordinary-call" => "p1.hashCode()", + "ordinary-arithmetic" => "p1 + 1", _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), }; var functionalType = shape switch { "typed-lambda" => "java.util.function.BiFunction", - "comparison" or "shift" or "invalid-comparison-bare" => "java.util.function.IntUnaryOperator", - "method-reference" or "generic-method-reference" or "invalid-generic-reference-bare" => "java.util.function.Supplier", + "comparison" or "shift" => "java.util.function.IntUnaryOperator", + "instanceof-generic" => "java.util.function.Function", + "method-reference" or "generic-method-reference" => "java.util.function.Supplier", _ => "java.util.function.Function", }; var proj = new XamarinAndroidApplicationProject { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs index bca18e29eb1..9ca4c8a580f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -403,6 +403,13 @@ public class NestedGenericLambdaSuperArgumentsActivity : Android.App.Activity public NestedGenericLambdaSuperArgumentsActivity () { } } +[Register ("my/app/InstanceOfGenericLambdaSuperArgumentsActivity")] +public class InstanceOfGenericLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1 instanceof java.util.Map ? p1 : p1")] + public InstanceOfGenericLambdaSuperArgumentsActivity () { } +} + [Register ("my/app/ComparisonLambdaSuperArgumentsActivity")] public class ComparisonLambdaSuperArgumentsActivity : Android.App.Activity { @@ -431,41 +438,6 @@ public class GenericMethodReferenceSuperArgumentsActivity : Android.App.Activity public GenericMethodReferenceSuperArgumentsActivity () { } } -[Register ("my/app/LambdaWithBareParameterActivity")] -public class LambdaWithBareParameterActivity : Android.App.Activity -{ - [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> p1, p1")] - public LambdaWithBareParameterActivity () { } -} - -[Register ("my/app/ParenthesizedLambdaWithBareParameterActivity")] -public class ParenthesizedLambdaWithBareParameterActivity : Android.App.Activity -{ - [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1, p1")] - public ParenthesizedLambdaWithBareParameterActivity () { } -} - -[Register ("my/app/GenericMethodReferenceWithBareParameterActivity")] -public class GenericMethodReferenceWithBareParameterActivity : Android.App.Activity -{ - [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1, p1")] - public GenericMethodReferenceWithBareParameterActivity () { } -} - -[Register ("my/app/GenericConstructionWithBareParameterActivity")] -public class GenericConstructionWithBareParameterActivity : Android.App.Activity -{ - [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry(p1, p1), p1")] - public GenericConstructionWithBareParameterActivity () { } -} - -[Register ("my/app/ComparisonWithBareParameterActivity")] -public class ComparisonWithBareParameterActivity : Android.App.Activity -{ - [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 < 2 ? p1 : 2, p1")] - public ComparisonWithBareParameterActivity () { } -} - [Register ("my/app/ExportMappedCtorActivity")] public class ExportMappedCtorActivity : Android.App.Activity { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index cf90158574d..46b3e5c992e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -246,11 +246,6 @@ public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed [InlineData ("MyApp.InvalidSuperArgumentsActivity", "p1")] [InlineData ("MyApp.NonCanonicalSuperArgumentZeroActivity", "p00")] [InlineData ("MyApp.NonCanonicalSuperArgumentOneActivity", "p01")] - [InlineData ("MyApp.LambdaWithBareParameterActivity", "p1 -> p1, p1")] - [InlineData ("MyApp.ParenthesizedLambdaWithBareParameterActivity", "(p1) -> p1, p1")] - [InlineData ("MyApp.GenericMethodReferenceWithBareParameterActivity", "Helper::p1, p1")] - [InlineData ("MyApp.GenericConstructionWithBareParameterActivity", "(p1) -> new SimpleEntry(p1, p1), p1")] - [InlineData ("MyApp.ComparisonWithBareParameterActivity", "(int p1) -> p1 < 2 ? p1 : 2, p1")] public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) { var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); @@ -278,6 +273,7 @@ public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, str [InlineData ("MyApp.LiteralCommaLambdaSuperArgumentsActivity")] [InlineData ("MyApp.GenericConstructionLambdaSuperArgumentsActivity")] [InlineData ("MyApp.NestedGenericLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.InstanceOfGenericLambdaSuperArgumentsActivity")] [InlineData ("MyApp.ComparisonLambdaSuperArgumentsActivity")] [InlineData ("MyApp.ShiftLambdaSuperArgumentsActivity")] [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")]