From 59665858aee9eb40047d42f0b23cbdcc0878e5c7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 09:14:41 +0200 Subject: [PATCH 1/7] [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 2/7] [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 3/7] [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 4/7] [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 5/7] [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 6/7] [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 7/7] [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 (