diff --git a/.gitattributes b/.gitattributes
index fa7720b69..fe3dfccc1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -35,3 +35,9 @@ Makefile eol=lf
*.wixproj eol=crlf
*.wxs eol=crlf
*.rtf eol=crlf
+
+# Gradle wrapper must stay LF on all platforms (executed under Bash on Unix)
+gradlew eol=lf
+*.properties eol=lf
+*.kt eol=lf
+*.kts eol=lf
diff --git a/src/Java.Interop.Localization/Resources.Designer.cs b/src/Java.Interop.Localization/Resources.Designer.cs
index 81ed44908..12164c404 100644
--- a/src/Java.Interop.Localization/Resources.Designer.cs
+++ b/src/Java.Interop.Localization/Resources.Designer.cs
@@ -429,6 +429,15 @@ public static string Generator_BG8C01 {
}
}
+ ///
+ /// Looks up a localized string similar to For type '{0}', the Kotlin name-mangled method '{1}' (originally '{2}') has multiple hash-suffixed siblings that erase to the same C# signature. Only the first will be emitted; remove the duplicate via Metadata.xml to suppress this warning..
+ ///
+ public static string Generator_BG8C02 {
+ get {
+ return ResourceManager.GetString("Generator_BG8C02", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Cannot generate Java wrapper for type '{0}'. Only 'class' types are supported..
///
diff --git a/src/Java.Interop.Localization/Resources.resx b/src/Java.Interop.Localization/Resources.resx
index 69753f855..084f94f50 100644
--- a/src/Java.Interop.Localization/Resources.resx
+++ b/src/Java.Interop.Localization/Resources.resx
@@ -308,6 +308,10 @@ The following terms should not be translated: Metadata.xml, path.
For type '{0}', base interface '{1}' is invalid.
{0}, {1} - .NET types.
+
+ For type '{0}', the Kotlin name-mangled method '{1}' (originally '{2}') has multiple hash-suffixed siblings that erase to the same C# signature. Only the first will be emitted; remove the duplicate via Metadata.xml to suppress this warning.
+ {0} - .NET type. {1} - C# method name. {2} - Original (mangled) JVM method name.
+
Cannot generate Java wrapper for type '{0}'. Only 'class' types are supported.
{0} - Java type.
diff --git a/src/Java.Interop.Tools.Generator/Utilities/Report.cs b/src/Java.Interop.Tools.Generator/Utilities/Report.cs
index 76fa1bc74..448504966 100644
--- a/src/Java.Interop.Tools.Generator/Utilities/Report.cs
+++ b/src/Java.Interop.Tools.Generator/Utilities/Report.cs
@@ -74,6 +74,7 @@ public LocalizedMessage (int code, string value)
public static LocalizedMessage WarningUnknownGenericConstraint => new LocalizedMessage (0x8B00, Localization.Resources.Generator_BG8B00);
public static LocalizedMessage WarningBaseInterfaceNotFound => new LocalizedMessage (0x8C00, Localization.Resources.Generator_BG8C00);
public static LocalizedMessage WarningBaseInterfaceInvalid => new LocalizedMessage (0x8C01, Localization.Resources.Generator_BG8C01);
+ public static LocalizedMessage WarningKotlinNameMangledCollision => new LocalizedMessage (0x8C02, Localization.Resources.Generator_BG8C02);
public static void LogCodedErrorAndExit (LocalizedMessage message, params string [] args)
=> LogCodedErrorAndExit (message, null, null, args);
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs b/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs
new file mode 100644
index 000000000..3a26fcd1f
--- /dev/null
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+using NUnit.Framework;
+using Xamarin.Android.Tools.Bytecode;
+
+namespace Xamarin.Android.Tools.BytecodeTests
+{
+ // Exercises the real Kotlin bytecode produced by the Gradle fixture under
+ // kotlin-gradle/ to confirm that the JVM-level mangling we expect (and that
+ // the generator's KotlinFixups must now de-collide) is actually what kotlinc
+ // emits for @JvmInline value-class parameters. See dotnet/java-interop#1431.
+ [TestFixture]
+ public class KotlinInlineClassCollisionTests : ClassFileFixture
+ {
+ [Test]
+ public void Widgets_HasCollidingHashMangledSiblings ()
+ {
+ var klass = LoadClassFile ("Widgets.class");
+
+ // Kotlin emits one mangled method per inline-class overload:
+ // tint-(J)V for MyColor (ULong-backed)
+ // tint-(J)V for MyAlpha (ULong-backed) — collides with MyColor
+ // tint-(F)V for MyDp (Float-backed) — unique
+ var tints = klass.Methods
+ .Where (m => m.Name.StartsWith ("tint-", StringComparison.Ordinal))
+ .ToList ();
+
+ Assert.AreEqual (3, tints.Count, "Expected three `tint-` overloads from the Gradle fixture.");
+
+ var longTints = tints.Where (m => m.Descriptor == "(J)V").ToList ();
+ Assert.AreEqual (2, longTints.Count,
+ "Expected two `tint-(J)V` siblings (MyColor + MyAlpha) — this is the multi-sibling collision case from dotnet/java-interop#1431.");
+
+ Assert.AreEqual (1, tints.Count (m => m.Descriptor == "(F)V"),
+ "Expected one unique `tint-(F)V` (MyDp) that should survive deduplication.");
+ }
+
+ [Test]
+ public void Widgets_HasNonCollidingHashMangledOverloads ()
+ {
+ var klass = LoadClassFile ("Widgets.class");
+
+ var pads = klass.Methods
+ .Where (m => m.Name.StartsWith ("pad-", StringComparison.Ordinal))
+ .ToList ();
+
+ Assert.AreEqual (2, pads.Count);
+ CollectionAssert.AreEquivalent (
+ new [] { "(F)F", "(FF)F" },
+ pads.Select (m => m.Descriptor).ToArray (),
+ "`pad` overloads have distinct JVM signatures and should both survive after rename.");
+ }
+
+ [Test]
+ public void InlineClasses_AreEmittedAsValueClasses ()
+ {
+ // Sanity check that @JvmInline really produced a JvmInline annotation on
+ // the inline-class type — this is what step (2) of #1431 will key on.
+ var myColor = LoadClassFile ("MyColor.class");
+
+ var annotations = myColor.Attributes
+ .OfType ()
+ .SelectMany (a => a.Annotations)
+ .Select (a => a.Type)
+ .ToList ();
+
+ Assert.Contains ("Lkotlin/jvm/JvmInline;", annotations);
+ }
+ }
+}
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj
index b7cadb7fa..e3c1546b7 100644
--- a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj
@@ -25,7 +25,13 @@
-
+
+
+
+
+
+
+
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets
index 0f2fcdfc8..c973c22db 100644
--- a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets
@@ -5,6 +5,12 @@
+
+
+
+
+
+
@@ -34,6 +40,25 @@
+
+
+ <_KotlinGradleProjectDir>$(MSBuildThisFileDirectory)kotlin-gradle
+
+
+
+
+
+
("compileKotlin") {
+ destinationDirectory.set(file("$rootDir/classes"))
+}
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/settings.gradle.kts b/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/settings.gradle.kts
new file mode 100644
index 000000000..6517d1d3a
--- /dev/null
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "kotlin-inline-class-fixtures"
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/src/main/kotlin/InlineClassCollisions.kt b/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/src/main/kotlin/InlineClassCollisions.kt
new file mode 100644
index 000000000..53f900aa5
--- /dev/null
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/src/main/kotlin/InlineClassCollisions.kt
@@ -0,0 +1,34 @@
+@file:JvmName("InlineClassCollisionsKt")
+
+package xat.bytecode.tests
+
+// Two distinct Kotlin inline classes that erase to the same JVM primitive (long).
+// Both `tint(MyColor)` and `tint(MyAlpha)` mangle to `tint-(J)V`, so they
+// collide once class-parse drops the hash suffix. This is the exact scenario
+// that Jetpack Compose triggers with Color/TextUnit/etc. and is the case
+// step (1) of dotnet/java-interop#1431 must handle.
+@JvmInline
+value class MyColor(val value: ULong)
+
+@JvmInline
+value class MyAlpha(val value: ULong)
+
+// A second inline class with a different backing primitive, so we can verify
+// that *non*-colliding hash siblings still survive.
+@JvmInline
+value class MyDp(val value: Float)
+
+object Widgets {
+
+ // Colliding pair: both erase to `tint-XXXXXXX(J)V`.
+ fun tint(color: MyColor) { /* no-op */ }
+ fun tint(alpha: MyAlpha) { /* no-op */ }
+
+ // Distinct hash-mangled sibling of the same source name — should survive
+ // alongside one of the `tint(long)` overloads.
+ fun tint(dp: MyDp) { /* no-op */ }
+
+ // A non-colliding pair: different arity, both hash-mangled.
+ fun pad(dp: MyDp): MyDp = dp
+ fun pad(dp1: MyDp, dp2: MyDp): MyDp = dp1
+}
diff --git a/tests/generator-Tests/Unit-Tests/KotlinFixupsTests.cs b/tests/generator-Tests/Unit-Tests/KotlinFixupsTests.cs
index ad3f650de..c920a83c9 100644
--- a/tests/generator-Tests/Unit-Tests/KotlinFixupsTests.cs
+++ b/tests/generator-Tests/Unit-Tests/KotlinFixupsTests.cs
@@ -1,6 +1,9 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
+using Java.Interop.Tools.Generator;
using Java.Interop.Tools.Generator.Transformation;
using MonoDroid.Generation;
using NUnit.Framework;
@@ -35,5 +38,127 @@ public void CreateMethod_EnsureKotlinHashcodeFix ()
Assert.IsTrue (klass.Methods [0].IsFinal);
Assert.IsFalse (klass.Methods [0].IsVirtual);
}
+
+ [Test, NonParallelizable]
+ public void CollidingHashSiblings_AreDeduplicated ()
+ {
+ // Two Kotlin hash-mangled siblings that erase to the same C# signature
+ // (one `long` parameter). After the rename to `Add` both would collide,
+ // so we keep only the first.
+ var xml = XDocument.Parse (@"
+
+
+
+
+
+
+
+
+ ");
+ var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), new CodeGenerationOptions ());
+
+ using var warnings = CaptureWarnings ();
+ KotlinFixups.Fixup (new [] { (GenBase) klass }.ToList ());
+
+ Assert.AreEqual (1, klass.Methods.Count, "Duplicate hash-mangled sibling should have been removed.");
+ Assert.AreEqual ("Add", klass.Methods [0].Name);
+ Assert.AreEqual ("add-AAAAAAA", klass.Methods [0].JavaName, "The first hash-mangled sibling in source order should survive.");
+ Assert.IsTrue (warnings.Messages.Any (m => m.Contains ("BG8C02")), "Expected BG8C02 warning, got: " + string.Join (Environment.NewLine, warnings.Messages));
+ }
+
+ [Test]
+ public void NonCollidingHashSiblings_AreBothKept ()
+ {
+ // Two siblings with distinct parameter lists: both should rename to `Add`
+ // and survive as overloads.
+ var xml = XDocument.Parse (@"
+
+
+
+
+
+
+
+
+ ");
+ var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), new CodeGenerationOptions ());
+
+ KotlinFixups.Fixup (new [] { (GenBase) klass }.ToList ());
+
+ Assert.AreEqual (2, klass.Methods.Count);
+ Assert.IsTrue (klass.Methods.All (m => m.Name == "Add"));
+ }
+
+ [Test]
+ public void MixedCollidingAndUniqueHashSiblings ()
+ {
+ // Three siblings of the same source-name: the long+long pair collide,
+ // the float arg is unique. Expect 2 methods to survive.
+ var xml = XDocument.Parse (@"
+
+
+
+
+
+
+
+
+
+
+
+ ");
+ var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), new CodeGenerationOptions ());
+
+ KotlinFixups.Fixup (new [] { (GenBase) klass }.ToList ());
+
+ Assert.AreEqual (2, klass.Methods.Count);
+ Assert.IsTrue (klass.Methods.All (m => m.Name == "Add"));
+ CollectionAssert.AreEquivalent (new [] { "long", "float" }, klass.Methods.Select (m => m.Parameters [0].RawNativeType).ToArray ());
+ }
+
+ [Test, NonParallelizable]
+ public void MangledMethod_CollidesWithNonMangledOverload ()
+ {
+ // A pre-existing non-mangled overload `add(long)` plus a mangled
+ // `add-AAAAAAA(long)` that also reduces to `add(long)` after rename.
+ // The mangled one is the duplicate -- drop it, keep the non-mangled.
+ var xml = XDocument.Parse (@"
+
+
+
+
+
+
+
+
+ ");
+ var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), new CodeGenerationOptions ());
+
+ using var warnings = CaptureWarnings ();
+ KotlinFixups.Fixup (new [] { (GenBase) klass }.ToList ());
+
+ Assert.AreEqual (1, klass.Methods.Count, "Mangled duplicate should have been removed, leaving the pre-existing non-mangled method.");
+ Assert.AreEqual ("add", klass.Methods [0].JavaName, "The kept method should be the non-mangled one.");
+ Assert.IsTrue (warnings.Messages.Any (m => m.Contains ("BG8C02")), "Expected BG8C02 warning.");
+ }
+
+ static WarningCapture CaptureWarnings () => new WarningCapture ();
+
+ sealed class WarningCapture : IDisposable
+ {
+ readonly Action previous;
+ public List Messages { get; } = new List ();
+
+ public WarningCapture ()
+ {
+ previous = Report.OutputDelegate;
+ Report.OutputDelegate = (level, msg) => Messages.Add (msg);
+ }
+
+ public void Dispose ()
+ {
+ Report.OutputDelegate = previous;
+ }
+ }
}
}
diff --git a/tools/generator/Java.Interop.Tools.Generator.Transformation/KotlinFixups.cs b/tools/generator/Java.Interop.Tools.Generator.Transformation/KotlinFixups.cs
index 680da243c..d7ba1a76c 100644
--- a/tools/generator/Java.Interop.Tools.Generator.Transformation/KotlinFixups.cs
+++ b/tools/generator/Java.Interop.Tools.Generator.Transformation/KotlinFixups.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Java.Interop.Tools.Generator;
using MonoDroid.Generation;
using MonoDroid.Utils;
@@ -23,7 +24,9 @@ private static void FixupClass (ClassGen c)
// We need to generate C# compatible names as well as prevent overriding
// them as we cannot generate JCW for them.
- foreach (var method in c.Methods.Where (m => m.IsKotlinNameMangled)) {
+ var mangled = c.Methods.Where (m => m.IsKotlinNameMangled).ToList ();
+
+ foreach (var method in mangled) {
// If the method is virtual, mark it as !virtual as it can't be overridden in Java
if (!method.IsFinal)
@@ -34,12 +37,46 @@ private static void FixupClass (ClassGen c)
FixMethodName (method);
}
+
+ RemoveCollidingSiblings (c, mangled);
}
private static void FixupInterface (InterfaceGen gen)
{
- foreach (var method in gen.Methods.Where (m => m.IsKotlinNameMangled))
+ var mangled = gen.Methods.Where (m => m.IsKotlinNameMangled).ToList ();
+
+ foreach (var method in mangled)
FixMethodName (method);
+
+ RemoveCollidingSiblings (gen, mangled);
+ }
+
+ // After the rename above, hash-mangled siblings can collide with each
+ // other AND with pre-existing non-mangled overloads. Both cases produce
+ // CS0111 in the generated code. Until step 2 of dotnet/java-interop#1431
+ // projects inline-class params as strongly-typed wrappers, drop the
+ // later mangled duplicate deterministically and warn so the user can
+ // override via Metadata.xml if desired. Non-mangled methods are always
+ // kept; only mangled methods are ever removed.
+ private static void RemoveCollidingSiblings (GenBase gen, List renamed)
+ {
+ if (renamed.Count == 0)
+ return;
+
+ foreach (var method in renamed) {
+ // Only treat `method` as the duplicate if a matching method
+ // (mangled or non-mangled) appears EARLIER in source order on
+ // the type. This keeps the first-declared overload and removes
+ // every later mangled sibling that collapses onto it.
+ var earlier = gen.Methods
+ .TakeWhile (m => m != method)
+ .FirstOrDefault (m => m.Name == method.Name && m.Matches (method));
+ if (earlier == null)
+ continue;
+
+ Report.LogCodedWarning (0, Report.WarningKotlinNameMangledCollision, method, gen.FullName, method.Name, method.JavaName);
+ gen.Methods.Remove (method);
+ }
}
private static void FixMethodName (Method method)