From c6bb0914809e3869ac28ffc531dbdf16fb5b84f7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 14:41:19 +0200 Subject: [PATCH 1/4] [tests] Cover duplicate JCW names Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../BuildWithLibraryTests.cs | 69 ++++++++++++++----- .../TrimmableTypeMapGeneratorTests.cs | 32 +++++++++ 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index dacb3c2ef96..f45bb2095b5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -619,26 +619,44 @@ public void AndroidLibraryProjectsZipWithOddPaths ([Values (AndroidRuntime.CoreC } } - [Test] - public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void DuplicateJCWNames (string typemapImplementation, AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - // TODO: NativeAOT should fail the application build - if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("NativeAOT doesn't fail the application build"); - } + var source1 = """ + namespace Library1; - var source = @"[Android.Runtime.Register (""examplelib.EmptyClass"")] public class EmptyClass : Java.Lang.Object { }"; + [Android.Runtime.Register ("examplelib.DuplicatePeer")] + public class FirstPeer : Java.Lang.Object + { + public FirstPeer (int value) + { + } + } + """; + var source2 = """ + namespace Library2; + + [Android.Runtime.Register ("examplelib.DuplicatePeer")] + public class SecondPeer : Java.Lang.Object + { + public SecondPeer (string value) + { + } + } + """; var library1 = new XamarinAndroidLibraryProject () { IsRelease = isRelease, ProjectName = "Library1", Sources = { - new BuildItem.Source ("EmptyClass.cs") { - TextContent = () => source + new BuildItem.Source ("FirstPeer.cs") { + TextContent = () => source1 } } }; @@ -647,8 +665,8 @@ public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.N IsRelease = isRelease, ProjectName = "Library2", Sources = { - new BuildItem.Source ("EmptyClass.cs") { - TextContent = () => source + new BuildItem.Source ("SecondPeer.cs") { + TextContent = () => source2 } } }; @@ -662,21 +680,40 @@ public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.N }, }; app.SetRuntime (runtime); - var projectPath = Path.Combine ("temp", TestName); + app.SetProperty ("AndroidTypeMapImplementation", typemapImplementation); + var projectPath = Path.Combine ("temp", $"{TestName}_{Guid.NewGuid ():N}"); using var lib1b = CreateDllBuilder (Path.Combine (projectPath, library1.ProjectName), cleanupAfterSuccessfulBuild: false); using var lib2b = CreateDllBuilder (Path.Combine (projectPath, library2.ProjectName), cleanupAfterSuccessfulBuild: false); Assert.IsTrue (lib1b.Build (library1), "Build of Library1 should have succeeded"); Assert.IsTrue (lib2b.Build (library2), "Build of Library2 should have succeeded"); - using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName)); + using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName), cleanupAfterSuccessfulBuild: false); appb.ThrowOnBuildFailure = false; Assert.IsFalse (appb.Build (app), "Build of App1 should have failed"); IEnumerable errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")); Assert.NotNull (errors, "Error should be XA4215"); - StringAssertEx.Contains ("EmptyClass", errors, "Error should mention the conflicting type name"); - StringAssertEx.Contains ("Library1", errors, "Error should mention all of the assemblies with conflicts"); - StringAssertEx.Contains ("Library2", errors, "Error should mention all of the assemblies with conflicts"); + StringAssertEx.Contains ("examplelib.DuplicatePeer", errors, "Error should mention the conflicting Java type name"); + StringAssertEx.Contains ("Library1.FirstPeer", errors, "Error should mention the first conflicting managed type"); + StringAssertEx.Contains ("Library2.SecondPeer", errors, "Error should mention the second conflicting managed type"); + Assert.IsFalse (appb.LastBuildOutput.Any (x => x.Contains ("warning XA4214")), + "Distinct managed type names should not produce XA4214."); + + var acwMapFile = appb.Output.GetIntermediaryPath ("acw-map.txt"); + if (typemapImplementation == "llvm-ir") { + var javaFile = appb.Output.GetIntermediaryPath (Path.Combine ("android", "src", "examplelib", "DuplicatePeer.java")); + FileAssert.Exists (javaFile, "llvm-ir generates JCW sources before XA4215 is detected."); + FileAssert.DoesNotExist (acwMapFile, "llvm-ir should not write an ambiguous acw-map."); + } else { + var typemapDirectory = appb.Output.GetIntermediaryPath ("typemap"); + var javaFile = Path.Combine (typemapDirectory, "java", "examplelib", "DuplicatePeer.java"); + FileAssert.DoesNotExist (javaFile, "A failed trimmable typemap build should not write an ambiguous JCW source."); + FileAssert.DoesNotExist (acwMapFile, "A failed trimmable typemap build should not write an ambiguous acw-map."); + if (Directory.Exists (typemapDirectory)) { + Assert.IsEmpty (Directory.GetFiles (typemapDirectory, "*.dll"), + "A failed trimmable typemap build should not write typemap assemblies."); + } + } } [Test] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 567df31d17f..938cb7fad36 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 LogDuplicateJavaTypeError (string javaName) => + logMessages.Add ($"XA4215: The Java type `{javaName}` is generated by more than one managed type."); + public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + logMessages.Add ($"XA4215: `{javaName}` generated by: {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) => @@ -98,6 +102,34 @@ public void ValidateJavaNames_ContextualKeywordInPackage_IsValid () Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); } + [Fact] + public void ValidateJavaNames_DuplicateGeneratedJcw_LogsXA4215 () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "examplelib/DuplicatePeer", + CompatJniName = "examplelib/DuplicatePeer", + ManagedTypeName = "Library1.FirstPeer", + ManagedTypeNamespace = "Library1", + ManagedTypeShortName = "FirstPeer", + AssemblyName = "Library1", + }, + new JavaPeerInfo { + JavaName = "examplelib/DuplicatePeer", + CompatJniName = "examplelib/DuplicatePeer", + ManagedTypeName = "Library2.SecondPeer", + ManagedTypeNamespace = "Library2", + ManagedTypeShortName = "SecondPeer", + AssemblyName = "Library2", + }, + }; + + Assert.False (CreateGenerator ().ValidateJavaNames (peers)); + Assert.Contains (logMessages, message => message.Contains ("XA4215") && message.Contains ("examplelib.DuplicatePeer")); + Assert.Contains (logMessages, message => message.Contains ("Library1.FirstPeer, Library1")); + Assert.Contains (logMessages, message => message.Contains ("Library2.SecondPeer, Library2")); + } + [Theory] [InlineData ("com/example/Outer$for", null, "for")] [InlineData ("com/example/Outer$record", null, "record")] From 510c8e03e123b3370d4e9519ac02109e3859d910 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 14:41:25 +0200 Subject: [PATCH 2/4] [typemap] Reject duplicate generated JCW names Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 2 ++ .../TrimmableTypeMapGenerator.cs | 18 ++++++++++++++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index ae25273fa8a..4c0113ff083 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 LogDuplicateJavaTypeError (string javaName); + void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index be49c266d47..93f2fb60ca5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -77,6 +77,24 @@ internal bool ValidateJavaNames (IReadOnlyList peers, string? appl { bool valid = true; var reportedNames = new HashSet (StringComparer.Ordinal); + foreach (var group in peers + .Where (ShouldGenerateJcw) + .GroupBy (peer => peer.JavaName, StringComparer.Ordinal) + .OrderBy (group => group.Key, StringComparer.Ordinal)) { + var firstAssemblyName = group.First ().AssemblyName; + if (group.All (peer => string.Equals (peer.AssemblyName, firstAssemblyName, StringComparison.Ordinal))) { + continue; + } + + var javaName = JniSignatureHelper.JniNameToJavaBinaryName (group.Key); + logger.LogDuplicateJavaTypeError (javaName); + foreach (var peer in group + .OrderBy (peer => peer.ManagedTypeName, StringComparer.Ordinal) + .ThenBy (peer => peer.AssemblyName, StringComparer.Ordinal)) { + logger.LogDuplicateJavaTypeDetailsError (javaName, $"{peer.ManagedTypeName}, {peer.AssemblyName}"); + } + valid = false; + } if (applicationJavaClass is not null && JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (applicationJavaClass, out var invalidApplicationIdentifier)) { ReportInvalidName (applicationJavaClass, invalidApplicationIdentifier); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 7cffd69bb6a..00ebb02c0eb 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 LogDuplicateJavaTypeError (string javaName) => + log.LogCodedError ("XA4215", Properties.Resources.XA4215, javaName); + public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + log.LogCodedError ("XA4215", Properties.Resources.XA4215_Details, javaName, managedTypeName); public void LogCustomJavaObjectError (string managedTypeName) => log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); public void LogCustomJavaObjectWarning (string managedTypeName) => From f1fc123e9739d1115c3e046f3b8f9780ce9a6120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Mon, 31 Aug 2026 11:37:57 +0200 Subject: [PATCH 3/4] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs | 4 ++-- .../Generator/TrimmableTypeMapGeneratorTests.cs | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index f45bb2095b5..d7378f387c5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -691,8 +691,8 @@ public SecondPeer (string value) using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName), cleanupAfterSuccessfulBuild: false); appb.ThrowOnBuildFailure = false; Assert.IsFalse (appb.Build (app), "Build of App1 should have failed"); - IEnumerable errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")); - Assert.NotNull (errors, "Error should be XA4215"); +var errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")).ToList (); +Assert.IsNotEmpty (errors, "Error should be XA4215"); StringAssertEx.Contains ("examplelib.DuplicatePeer", errors, "Error should mention the conflicting Java type name"); StringAssertEx.Contains ("Library1.FirstPeer", errors, "Error should mention the first conflicting managed type"); StringAssertEx.Contains ("Library2.SecondPeer", errors, "Error should mention the second conflicting managed type"); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 938cb7fad36..7d91217c42c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -125,10 +125,9 @@ public void ValidateJavaNames_DuplicateGeneratedJcw_LogsXA4215 () }; Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ("XA4215") && message.Contains ("examplelib.DuplicatePeer")); - Assert.Contains (logMessages, message => message.Contains ("Library1.FirstPeer, Library1")); - Assert.Contains (logMessages, message => message.Contains ("Library2.SecondPeer, Library2")); - } + Assert.Contains (logMessages, message => message == "XA4215: The Java type `examplelib.DuplicatePeer` is generated by more than one managed type."); + Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library1.FirstPeer, Library1"); + Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library2.SecondPeer, Library2"); [Theory] [InlineData ("com/example/Outer$for", null, "for")] From 7157e3a6a5b085447599e574c411f189fe4a49bc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 2 Sep 2026 15:34:14 +0200 Subject: [PATCH 4/4] [tests] Fix duplicate JCW test syntax Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 7d91217c42c..5d47ff56e4d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -128,6 +128,7 @@ public void ValidateJavaNames_DuplicateGeneratedJcw_LogsXA4215 () Assert.Contains (logMessages, message => message == "XA4215: The Java type `examplelib.DuplicatePeer` is generated by more than one managed type."); Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library1.FirstPeer, Library1"); Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library2.SecondPeer, Library2"); + } [Theory] [InlineData ("com/example/Outer$for", null, "for")]