Skip to content

Commit d11ce38

Browse files
committed
[Xamarin.Android.Tools.Bytecode] Improve Kotlin metadata matching.
1 parent 32635fd commit d11ce38

File tree

10 files changed

+182
-45
lines changed

10 files changed

+182
-45
lines changed

src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinClassMetadata.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,9 @@ public class KotlinType
358358
};
359359
}
360360

361-
public string GetSignature ()
361+
public string GetSignature (bool convertUnsignedToPrimitive = true)
362362
{
363-
return KotlinUtilities.ConvertKotlinTypeSignature (this);
363+
return KotlinUtilities.ConvertKotlinTypeSignature (this, null, convertUnsignedToPrimitive);
364364
}
365365
}
366366

src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,12 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl
175175
return;
176176
}
177177

178-
var java_parameters = method.GetFilteredParameters ();
178+
(var start, var end) = CreateParameterMap (method, metadata, kotlinClass);
179179

180-
for (var i = 0; i < java_parameters.Length; i++) {
181-
var java_p = java_parameters [i];
180+
var java_parameters = method.GetParameters ();
181+
182+
for (var i = 0; i < end - start; i++) {
183+
var java_p = java_parameters [start + i];
182184
var kotlin_p = metadata.ValueParameters == null ? null : metadata.ValueParameters [i];
183185
if (kotlin_p == null || kotlin_p.Type == null || kotlin_p.Name == null)
184186
continue;
@@ -197,6 +199,46 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl
197199
method.KotlinReturnType = GetKotlinType (method.ReturnType.TypeSignature, metadata.ReturnType?.ClassName);
198200
}
199201

202+
public static (int start, int end) CreateParameterMap (MethodInfo method, KotlinFunction function, KotlinClass? kotlinClass)
203+
{
204+
var parameters = method.GetParameters ();
205+
var start = 0;
206+
var end = parameters.Length;
207+
208+
// If the parameter counts are the same, that's good enough (because we know signatures matched)
209+
if (IsValidParameterMap (method, function, start, end))
210+
return (start, end);
211+
212+
// Remove the "hidden" receiver type parameter from the start of the parameter list
213+
if (function.ReceiverType != null)
214+
start++;
215+
216+
if (IsValidParameterMap (method, function, start, end))
217+
return (start, end);
218+
219+
var last_p = parameters.Last ();
220+
221+
// Remove the "hidden" coroutine continuation type parameter from the end of the parameter list
222+
// We try to restrict it to compiler generated paramteres because a user might have actually used it as a parameter
223+
if (last_p.Type.BinaryName == "Lkotlin/coroutines/Continuation;" && (last_p.IsUnnamedParameter () || last_p.IsCompilerNamed ()))
224+
end--;
225+
226+
if (IsValidParameterMap (method, function, start, end))
227+
return (start, end);
228+
229+
// Remove the "hidden" "this" type parameter for a static method from the start of the parameter list
230+
// Note we do this last because sometimes it isn't there.
231+
if (method.AccessFlags.HasFlag (MethodAccessFlags.Static))
232+
start++;
233+
234+
if (IsValidParameterMap (method, function, start, end))
235+
return (start, end);
236+
237+
return (0, 0);
238+
}
239+
240+
static bool IsValidParameterMap (MethodInfo method, KotlinFunction function, int start, int end) => function.ValueParameters?.Count == end - start;
241+
200242
static void FixupExtensionMethod (MethodInfo method)
201243
{
202244
// Kotlin "extension" methods give the first parameter an ugly name
@@ -271,10 +313,23 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
271313

272314
static MethodInfo? FindJavaMethod (KotlinFile kotlinFile, KotlinFunction function, ClassFile klass)
273315
{
274-
var possible_methods = klass.Methods.Where (method => method.Name == function.JvmName &&
275-
method.GetFilteredParameters ().Length == function.ValueParameters?.Count);
316+
var possible_methods = klass.Methods.Where (method => method.Name == function.JvmName).ToArray ();
317+
var signature = function.JvmSignature;
318+
319+
// If the Descriptor/Signature match, that means all parameters and return type match
320+
if (signature != null && possible_methods.SingleOrDefault (method => method.Descriptor == signature) is MethodInfo m)
321+
return m;
322+
323+
// Sometimes JvmSignature is null (or unhelpful), so we're going to construct one ourselves and see if they match
324+
signature = function.ConstructJvmSignature ();
325+
326+
if (possible_methods.SingleOrDefault (method => method.Descriptor == signature) is MethodInfo m2)
327+
return m2;
276328

277-
foreach (var method in possible_methods) {
329+
// If that didn't work, let's do it the hard way!
330+
// I don't know if this catches anything additional, but it was the original code we shipped, so
331+
// we'll keep it just in case something in the wild requires it.
332+
foreach (var method in possible_methods.Where (method => method.GetFilteredParameters ().Length == function.ValueParameters?.Count)) {
278333
if (function.ReturnType == null)
279334
continue;
280335
if (!TypesMatch (method.ReturnType, function.ReturnType, kotlinFile))
@@ -286,6 +341,8 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
286341
return method;
287342
}
288343

344+
// Theoretically this should never be hit, but who knows. At worst, it just means
345+
// Kotlin niceties won't be applied to the method.
289346
return null;
290347
}
291348

src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinUtilities.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Xamarin.Android.Tools.Bytecode
88
{
99
public static class KotlinUtilities
1010
{
11-
public static string ConvertKotlinTypeSignature (KotlinType? type, KotlinFile? metadata = null)
11+
public static string ConvertKotlinTypeSignature (KotlinType? type, KotlinFile? metadata = null, bool convertUnsignedToPrimitive = true)
1212
{
1313
if (type is null)
1414
return string.Empty;
@@ -27,15 +27,15 @@ public static string ConvertKotlinTypeSignature (KotlinType? type, KotlinFile? m
2727
return "Ljava/lang/Object;";
2828
}
2929

30-
var result = ConvertKotlinClassToJava (class_name);
30+
var result = ConvertKotlinClassToJava (class_name, convertUnsignedToPrimitive);
3131

3232
if (result == "[")
33-
result += ConvertKotlinTypeSignature (type.Arguments?.FirstOrDefault ()?.Type);
33+
result += ConvertKotlinTypeSignature (type.Arguments?.FirstOrDefault ()?.Type, null, convertUnsignedToPrimitive);
3434

3535
return result;
3636
}
3737

38-
public static string ConvertKotlinClassToJava (string? className)
38+
public static string ConvertKotlinClassToJava (string? className, bool convertUnsignedToPrimitive = true)
3939
{
4040
if (className == null || string.IsNullOrWhiteSpace (className))
4141
return string.Empty;
@@ -45,6 +45,9 @@ public static string ConvertKotlinClassToJava (string? className)
4545
if (type_map.TryGetValue (className.TrimEnd (';'), out var result))
4646
return result;
4747

48+
if (convertUnsignedToPrimitive && unsigned_type_map.TryGetValue (className.TrimEnd (';'), out var result2))
49+
return result2;
50+
4851
return "L" + className;
4952
}
5053

@@ -92,6 +95,14 @@ public static bool IsDefaultConstructorMarker (this MethodInfo method)
9295
parameters [parameters.Length - 1].Type.TypeSignature == "Lkotlin/jvm/internal/DefaultConstructorMarker;";
9396
}
9497

98+
// Sometimes the Kotlin provided JvmSignature is null (or unhelpful), so we need to construct one ourselves
99+
public static string ConstructJvmSignature (this KotlinFunction function)
100+
{
101+
// The receiver type (if specified) is a "hidden" parameter passed at the beginning
102+
// of the Java parameter list, so we include it so the Signature/Descriptors match.
103+
return $"({function.ReceiverType?.GetSignature (false)}{string.Concat (function.ValueParameters?.Select (p => p.Type?.GetSignature (false)) ?? Enumerable.Empty<string> ())}){function.ReturnType?.GetSignature (false)}";
104+
}
105+
95106
internal static List<TResult>? ToList<TSource, TResult> (this IEnumerable<TSource>? self, JvmNameResolver resolver, Func<TSource, JvmNameResolver, TResult?> creator)
96107
where TResult: class
97108
{
@@ -114,37 +125,42 @@ public static bool IsDefaultConstructorMarker (this MethodInfo method)
114125

115126
public static bool IsUnnamedParameter (this ParameterInfo parameter) => parameter.Name.Length > 1 && parameter.Name.StartsWith ("p", StringComparison.Ordinal) && int.TryParse (parameter.Name.Substring (1), out var _);
116127

128+
public static bool IsCompilerNamed (this ParameterInfo parameter) => parameter.Name.Length > 0 && parameter.Name.StartsWith ("$", StringComparison.Ordinal);
129+
117130
public static bool IsUnnamedParameter (this KotlinValueParameter parameter) => parameter.Name?.Length > 1 &&
118131
parameter.Name.StartsWith ("p", StringComparison.Ordinal) &&
119132
int.TryParse (parameter.Name.Substring (1), out var _);
120133

134+
static Dictionary<string, string> unsigned_type_map = new Dictionary<string, string> {
135+
{ "kotlin/UInt", "I" },
136+
{ "kotlin/ULong", "J" },
137+
{ "kotlin/UShort", "S" },
138+
{ "kotlin/UByte", "B" },
139+
{ "kotlin/UIntArray", "[I" },
140+
{ "kotlin/ULongArray", "[J" },
141+
{ "kotlin/UShortArray", "[S" },
142+
{ "kotlin/UByteArray", "[B" },
143+
};
144+
121145
static Dictionary<string, string> type_map = new Dictionary<string, string> {
122146
{ "kotlin/Int", "I" },
123-
{ "kotlin/UInt", "I" },
124147
{ "kotlin/Double", "D" },
125148
{ "kotlin/Char", "C" },
126149
{ "kotlin/Long", "J" },
127-
{ "kotlin/ULong", "J" },
128150
{ "kotlin/Float", "F" },
129151
{ "kotlin/Short", "S" },
130-
{ "kotlin/UShort", "S" },
131152
{ "kotlin/Byte", "B" },
132-
{ "kotlin/UByte", "B" },
133153
{ "kotlin/Boolean", "Z" },
134154
{ "kotlin/Unit", "V" },
135155

136156
{ "kotlin/Array", "[" },
137157
{ "kotlin/IntArray", "[I" },
138-
{ "kotlin/UIntArray", "[I" },
139158
{ "kotlin/DoubleArray", "[D" },
140159
{ "kotlin/CharArray", "[C" },
141160
{ "kotlin/LongArray", "[J" },
142-
{ "kotlin/ULongArray", "[J" },
143161
{ "kotlin/FloatArray", "[F" },
144162
{ "kotlin/ShortArray", "[S" },
145-
{ "kotlin/UShortArray", "[S" },
146163
{ "kotlin/ByteArray", "[B" },
147-
{ "kotlin/UByteArray", "[B" },
148164
{ "kotlin/BooleanArray", "[Z" },
149165

150166
{ "kotlin/Any", "Ljava/lang/Object;" },

tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileFixture.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,46 @@ protected static void AssertXmlDeclaration (string[] classResources, string xmlR
105105
Assert.AreEqual (expected, actual.ToString ());
106106
}
107107

108+
protected static KotlinMetadata GetMetadataForClass (ClassFile klass)
109+
{
110+
var attr = klass.Attributes.OfType<RuntimeVisibleAnnotationsAttribute> ().FirstOrDefault ();
111+
var kotlin = attr?.Annotations.FirstOrDefault (a => a.Type == "Lkotlin/Metadata;");
112+
113+
Assert.NotNull (kotlin);
114+
115+
var meta = KotlinMetadata.FromAnnotation (kotlin);
116+
117+
return meta;
118+
}
119+
120+
protected static KotlinClass GetClassMetadataForClass (ClassFile klass)
121+
{
122+
var meta = GetMetadataForClass (klass);
123+
Assert.AreEqual (KotlinMetadataKind.Class, meta.Kind);
124+
125+
return meta.AsClassMetadata ();
126+
}
127+
128+
protected static KotlinFile GetFileMetadataForClass (ClassFile klass)
129+
{
130+
var meta = GetMetadataForClass (klass);
131+
Assert.AreEqual (KotlinMetadataKind.File, meta.Kind);
132+
133+
return meta.AsFileMetadata ();
134+
}
135+
136+
protected static KotlinMetadata GetMetadataForClassFile (string file)
137+
{
138+
var c = LoadClassFile (file);
139+
return GetMetadataForClass (c);
140+
}
141+
142+
protected static KotlinClass GetClassMetadata (string file)
143+
{
144+
var c = LoadClassFile (file);
145+
return GetClassMetadataForClass (c);
146+
}
147+
108148
static Stream GetResourceStream (string resource)
109149
{
110150
// Look for resources that end with our name, this allows us to

tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinFixupsTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,5 +310,53 @@ public void HandleKotlinNameShadowing ()
310310
Assert.True (klass.Methods.Single (m => m.Name == "hitCount").AccessFlags.HasFlag (MethodAccessFlags.Public));
311311
Assert.True (klass.Methods.Single (m => m.Name == "setType").AccessFlags.HasFlag (MethodAccessFlags.Public));
312312
}
313+
314+
[Test]
315+
public void MatchParametersWithReceiver ()
316+
{
317+
var klass = LoadClassFile ("DeepRecursiveKt.class");
318+
var meta = GetFileMetadataForClass (klass);
319+
320+
var java_method = klass.Methods.Single (m => m.Name == "invoke");
321+
var kotlin_function = meta.Functions.Single (m => m.Name == "invoke");
322+
323+
(var start, var end) = KotlinFixups.CreateParameterMap (java_method, kotlin_function, null);
324+
325+
// Start is 1 instead of 0 to skip the receiver
326+
Assert.AreEqual (1, start);
327+
Assert.AreEqual (2, end);
328+
}
329+
330+
[Test]
331+
public void MatchParametersWithContinuation ()
332+
{
333+
var klass = LoadClassFile ("DeepRecursiveScope.class");
334+
var meta = GetClassMetadataForClass (klass);
335+
336+
var java_method = klass.Methods.Single (m => m.Name == "callRecursive" && m.GetParameters ().Count () == 2);
337+
var kotlin_function = meta.Functions.Single (m => m.Name == "callRecursive" && m.JvmSignature.Count (c => c == ';') == 3);
338+
339+
(var start, var end) = KotlinFixups.CreateParameterMap (java_method, kotlin_function, null);
340+
341+
// End is 1 instead of 2 to skip the trailing continuation
342+
Assert.AreEqual (0, start);
343+
Assert.AreEqual (1, end);
344+
}
345+
346+
[Test]
347+
public void MatchParametersWithStaticThis ()
348+
{
349+
var klass = LoadClassFile ("UByteArray.class");
350+
var meta = GetClassMetadataForClass (klass);
351+
352+
var java_method = klass.Methods.Single (m => m.Name == "contains-7apg3OU" && m.GetParameters ().Count () == 2);
353+
var kotlin_function = meta.Functions.Single (m => m.Name == "contains");
354+
355+
(var start, var end) = KotlinFixups.CreateParameterMap (java_method, kotlin_function, null);
356+
357+
// Start is 1 instead of 0 to skip the static 'this'
358+
Assert.AreEqual (1, start);
359+
Assert.AreEqual (2, end);
360+
}
313361
}
314362
}

tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinMetadataTests.cs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,6 @@ public void KotlinExtensionMethods ()
201201
Assert.AreEqual (KotlinClassInheritability.Final, klass_meta.Inheritability);
202202
Assert.AreEqual (KotlinClassVisibility.Public, klass_meta.Visibility);
203203
}
204-
205-
private KotlinMetadata GetMetadataForClassFile (string file)
206-
{
207-
var c = LoadClassFile (file);
208-
var attr = c.Attributes.OfType<RuntimeVisibleAnnotationsAttribute> ().FirstOrDefault ();
209-
var kotlin = attr?.Annotations.FirstOrDefault (a => a.Type == "Lkotlin/Metadata;");
210-
211-
Assert.NotNull (kotlin);
212-
213-
var meta = KotlinMetadata.FromAnnotation (kotlin);
214-
215-
return meta;
216-
}
217-
218-
private KotlinClass GetClassMetadata (string file)
219-
{
220-
var meta = GetMetadataForClassFile (file);
221-
222-
Assert.AreEqual (KotlinMetadataKind.Class, meta.Kind);
223-
224-
Assert.AreEqual ("1.0.3", meta.ByteCodeVersion.ToString ());
225-
Assert.AreEqual ("1.1.15", meta.MetadataVersion.ToString ());
226-
227-
return meta.AsClassMetadata ();
228-
}
229204
}
230205
}
231206

Binary file not shown.
Binary file not shown.
Binary file not shown.

tools/generator/generator.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"tests\\Xamarin.Android.Tools.Bytecode-Tests\\Xamarin.Android.Tools.Bytecode-Tests.csproj",
1616
"tests\\Xamarin.SourceWriter-Tests\\Xamarin.SourceWriter-Tests.csproj",
1717
"tests\\generator-Tests\\generator-Tests.csproj",
18+
"tools\\class-parse\\class-parse.csproj",
1819
"tools\\generator\\generator.csproj"
1920
]
2021
}

0 commit comments

Comments
 (0)