Skip to content

Commit 52ccc51

Browse files
Add unit tests for covariant return type override detection
When a derived type overrides a base method with a narrower return type, the JCW must use the base method's JNI signature: // Base: [Register ("getResult", "()Ljava/lang/Object;", "...")] public virtual Java.Lang.Object GetResult () => null; // User override — narrower return, no [Register]: public override Java.Lang.Object GetResult () => myString; // JCW must use base's "()Ljava/lang/Object;", not derived's type. Also addresses review feedback: consolidated tests, JNI-based ExportField type resolution, consistent signature-based dedup keys. Part of #10933 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7530c54 commit 52ccc51

9 files changed

Lines changed: 203 additions & 261 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 45 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<string, JavaPeerInfo> results
207207
// Override and interface detection is only for user ACW class types:
208208
// - MCW types (DoNotGenerateAcw) already have [Register] on every method
209209
// - Interface types don't implement other interfaces' methods in JCWs
210-
var marshalMethods = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface);
210+
var (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface);
211211

212212
// Resolve activation constructor
213213
var activationCtor = ResolveActivationCtor (fullName, typeDef, index);
@@ -232,7 +232,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<string, JavaPeerInfo> results
232232
IsUnconditional = isUnconditional,
233233
MarshalMethods = marshalMethods,
234234
JavaConstructors = BuildJavaConstructors (marshalMethods),
235-
JavaFields = CollectExportFields (typeDef, index),
235+
JavaFields = exportFields,
236236
ActivationCtor = activationCtor,
237237
InvokerTypeName = invokerTypeName,
238238
IsGenericDefinition = isGenericDefinition,
@@ -242,14 +242,19 @@ void ScanAssembly (AssemblyIndex index, Dictionary<string, JavaPeerInfo> results
242242
}
243243
}
244244

245-
List<MarshalMethodInfo> CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index, bool detectBaseOverrides)
245+
(List<MarshalMethodInfo>, List<JavaFieldInfo>) CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index, bool detectBaseOverrides)
246246
{
247247
var methods = new List<MarshalMethodInfo> ();
248+
var fields = new List<JavaFieldInfo> ();
248249
var registeredMethodKeys = new HashSet<string> (StringComparer.Ordinal);
249250

250-
// Pass 1: collect methods with [Register] or [Export] directly on them
251+
// Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them
251252
foreach (var methodHandle in typeDef.GetMethods ()) {
252253
var methodDef = index.Reader.GetMethodDefinition (methodHandle);
254+
255+
// Check for [ExportField] — produces both a marshal method AND a field
256+
CollectExportField (methodDef, index, fields);
257+
253258
if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) {
254259
continue;
255260
}
@@ -300,7 +305,7 @@ List<MarshalMethodInfo> CollectMarshalMethods (TypeDefinition typeDef, AssemblyI
300305
CollectBaseConstructorChain (typeDef, index, methods);
301306
}
302307

303-
return methods;
308+
return (methods, fields);
304309
}
305310

306311
/// <summary>
@@ -380,14 +385,16 @@ void CollectBasePropertyOverrides (TypeDefinition typeDef, AssemblyIndex index,
380385
}
381386

382387
var getterName = index.Reader.GetString (getterDef.Name);
383-
if (alreadyRegistered.Contains (getterName)) {
388+
var sig = getterDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
389+
var sigKey = $"{getterName}({string.Join (",", sig.ParameterTypes)})";
390+
if (alreadyRegistered.Contains (sigKey)) {
384391
continue;
385392
}
386393

387394
var baseRegistration = FindBaseRegisteredProperty (typeDef, index, getterName, getterDef);
388395
if (baseRegistration is not null) {
389396
methods.Add (baseRegistration);
390-
alreadyRegistered.Add (getterName);
397+
alreadyRegistered.Add (sigKey);
391398
}
392399
}
393400
}
@@ -1413,75 +1420,44 @@ static List<JavaConstructorInfo> BuildJavaConstructors (List<MarshalMethodInfo>
14131420
}
14141421

14151422
/// <summary>
1416-
/// Collects Java field declarations from [ExportField] attributes on methods.
1417-
/// Each [ExportField("name")] on a method produces a Java field initialized by
1418-
/// calling that method.
1423+
/// Checks a single method for [ExportField] and adds a JavaFieldInfo if found.
1424+
/// Called inline during Pass 1 to avoid a separate iteration.
14191425
/// </summary>
1420-
static List<JavaFieldInfo> CollectExportFields (TypeDefinition typeDef, AssemblyIndex index)
1426+
static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<JavaFieldInfo> fields)
14211427
{
1422-
var fields = new List<JavaFieldInfo> ();
1423-
1424-
foreach (var methodHandle in typeDef.GetMethods ()) {
1425-
var methodDef = index.Reader.GetMethodDefinition (methodHandle);
1426-
1427-
foreach (var caHandle in methodDef.GetCustomAttributes ()) {
1428-
var ca = index.Reader.GetCustomAttribute (caHandle);
1429-
var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
1430-
1431-
if (attrName != "ExportFieldAttribute") {
1432-
continue;
1433-
}
1434-
1435-
var value = index.DecodeAttribute (ca);
1436-
if (value.FixedArguments.Length == 0) {
1437-
continue;
1438-
}
1428+
foreach (var caHandle in methodDef.GetCustomAttributes ()) {
1429+
var ca = index.Reader.GetCustomAttribute (caHandle);
1430+
var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
14391431

1440-
var fieldName = (string?)value.FixedArguments [0].Value;
1441-
if (fieldName is null) {
1442-
continue;
1443-
}
1432+
if (attrName != "ExportFieldAttribute") {
1433+
continue;
1434+
}
14441435

1445-
var managedName = index.Reader.GetString (methodDef.Name);
1446-
var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
1447-
var javaReturnType = ManagedReturnTypeToJava (sig.ReturnType);
1448-
var access = GetJavaAccess (methodDef.Attributes & MethodAttributes.MemberAccessMask);
1449-
var isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0;
1450-
1451-
fields.Add (new JavaFieldInfo {
1452-
FieldName = fieldName,
1453-
JavaTypeName = javaReturnType,
1454-
InitializerMethodName = managedName,
1455-
Visibility = access,
1456-
IsStatic = isStatic,
1457-
});
1436+
var value = index.DecodeAttribute (ca);
1437+
if (value.FixedArguments.Length == 0) {
1438+
continue;
14581439
}
1459-
}
14601440

1461-
return fields;
1462-
}
1441+
var fieldName = (string?)value.FixedArguments [0].Value;
1442+
if (fieldName is null) {
1443+
continue;
1444+
}
14631445

1464-
static string ManagedReturnTypeToJava (string managedType)
1465-
{
1466-
switch (managedType) {
1467-
case "System.String": return "java.lang.String";
1468-
case "System.Boolean": return "boolean";
1469-
case "System.Byte":
1470-
case "System.SByte": return "byte";
1471-
case "System.Char": return "char";
1472-
case "System.Int16":
1473-
case "System.UInt16": return "short";
1474-
case "System.Int32":
1475-
case "System.UInt32": return "int";
1476-
case "System.Int64":
1477-
case "System.UInt64": return "long";
1478-
case "System.Single": return "float";
1479-
case "System.Double": return "double";
1480-
case "System.Void": return "void";
1481-
default:
1482-
// For reference types, use java.lang.Object as fallback.
1483-
// The exact Java type would require JNI signature resolution.
1484-
return "java.lang.Object";
1446+
var managedName = index.Reader.GetString (methodDef.Name);
1447+
var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
1448+
var jniSig = BuildJniSignatureFromManaged (sig);
1449+
var jniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig);
1450+
var javaReturnType = JniSignatureHelper.JniTypeToJava (jniReturnType);
1451+
var access = GetJavaAccess (methodDef.Attributes & MethodAttributes.MemberAccessMask);
1452+
var isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0;
1453+
1454+
fields.Add (new JavaFieldInfo {
1455+
FieldName = fieldName,
1456+
JavaTypeName = javaReturnType,
1457+
InitializerMethodName = managedName,
1458+
Visibility = access,
1459+
IsStatic = isStatic,
1460+
});
14851461
}
14861462
}
14871463
}

tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,8 @@ public static (Dictionary<string, TypeComparisonData> perType, List<TypeMapEntry
110110
javaCtorSignatures.Add (ctor.JniSignature);
111111
}
112112
}
113-
} catch {
114-
// Some types may fail CecilImporter (e.g., missing base types).
115-
// Fall back to direct attribute scanning.
113+
} catch (Exception ex) {
114+
System.Diagnostics.Debug.WriteLine ($"CecilImporter.CreateType failed for {typeDef.FullName}: {ex.Message}");
116115
ExtractDirectRegisterCtors (typeDef, javaCtorSignatures);
117116
}
118117
} else {

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ConstructorSuperArgsTests.cs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.IO;
23
using System.Linq;
34
using Xunit;
@@ -6,58 +7,35 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
67

78
/// <summary>
89
/// Tests for constructor super() argument matching.
9-
/// The legacy pipeline selects super() arguments based on base registered ctors:
10-
/// - Compatible params → forward all (super(p0, p1, ...))
11-
/// - No compatible params, parameterless base exists → super()
12-
/// - [Export(SuperArgumentsString="...")] → use custom string
1310
/// </summary>
1411
public class ConstructorSuperArgsTests : FixtureTestBase
1512
{
16-
static string GenerateToString (JavaPeerInfo type)
17-
{
18-
var generator = new JcwJavaSourceGenerator ();
19-
using var writer = new StringWriter ();
20-
generator.Generate (type, writer);
21-
return writer.ToString ();
22-
}
23-
2413
[Fact]
2514
public void MatchingBaseCtor_ForwardsAllParams ()
2615
{
27-
// UserActivity extends Activity which has [Register(".ctor","()V")]
28-
// UserActivity has activation ctor (IntPtr, JniHandleOwnership) — not a user ctor
29-
// The base ()V ctor should be inherited as a seed constructor
3016
var peer = FindFixtureByJavaName ("my/app/UserActivity");
3117
Assert.NotEmpty (peer.JavaConstructors);
3218
var ctor = peer.JavaConstructors.First (c => c.JniSignature == "()V");
33-
// null SuperArgumentsString means "forward all params"
3419
Assert.Null (ctor.SuperArgumentsString);
3520
}
3621

3722
[Fact]
3823
public void CustomParamCtor_FallsBackToEmptySuper ()
3924
{
40-
// CustomParamActivity has ctor(string, int) which doesn't match Activity's ()V
41-
// But Activity has parameterless ctor → fallback to super()
4225
var peer = FindFixtureByJavaName ("my/app/CustomParamActivity");
4326
var customCtor = peer.JavaConstructors.FirstOrDefault (c => c.JniSignature != "()V");
4427
Assert.NotNull (customCtor);
4528
Assert.Equal ("", customCtor.SuperArgumentsString);
46-
}
4729

48-
[Fact]
49-
public void CustomParamCtor_JcwEmitsEmptySuper ()
50-
{
51-
var peer = FindFixtureByJavaName ("my/app/CustomParamActivity");
52-
var java = GenerateToString (peer);
53-
// The custom-param ctor should call super() not super(p0, p1)
54-
Assert.Contains ("super ();", java);
30+
var generator = new JcwJavaSourceGenerator ();
31+
using var writer = new StringWriter ();
32+
generator.Generate (peer, writer);
33+
Assert.Contains ("super ();", writer.ToString ());
5534
}
5635

5736
[Fact]
5837
public void ExportSuperArgs_UsesCustomString ()
5938
{
60-
// From existing test fixtures — uses SuperArgumentsString
6139
var type = new JavaPeerInfo {
6240
JavaName = "my/app/ExportCtorTest",
6341
CompatJniName = "my/app/ExportCtorTest",
@@ -66,7 +44,7 @@ public void ExportSuperArgs_UsesCustomString ()
6644
ManagedTypeShortName = "ExportCtorTest",
6745
AssemblyName = "App",
6846
BaseJavaName = "android/app/Service",
69-
JavaConstructors = new System.Collections.Generic.List<JavaConstructorInfo> {
47+
JavaConstructors = new List<JavaConstructorInfo> {
7048
new JavaConstructorInfo {
7149
JniSignature = "(Landroid/content/Context;I)V",
7250
ConstructorIndex = 0,
@@ -75,7 +53,10 @@ public void ExportSuperArgs_UsesCustomString ()
7553
},
7654
};
7755

78-
var java = GenerateToString (type);
56+
var generator = new JcwJavaSourceGenerator ();
57+
using var writer = new StringWriter ();
58+
generator.Generate (type, writer);
59+
var java = writer.ToString ();
7960
Assert.Contains ("super (p0);", java);
8061
Assert.DoesNotContain ("super (p0, p1);", java);
8162
}

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportAccessModifierTests.cs

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,20 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
99
/// </summary>
1010
public class ExportAccessModifierTests : FixtureTestBase
1111
{
12-
static string GenerateToString (JavaPeerInfo type)
13-
{
14-
var generator = new JcwJavaSourceGenerator ();
15-
using var writer = new StringWriter ();
16-
generator.Generate (type, writer);
17-
return writer.ToString ();
18-
}
19-
20-
[Fact]
21-
public void Scanner_ExportMethod_HasIsExportTrue ()
22-
{
23-
var peer = FindFixtureByJavaName ("my/app/ExportAccessTest");
24-
var publicMethod = peer.MarshalMethods.First (m => m.JniName == "publicMethod");
25-
Assert.True (publicMethod.IsExport);
26-
}
27-
2812
[Fact]
29-
public void Scanner_ExportMethod_HasCorrectJavaAccess ()
13+
public void Scanner_ExportMethods_HaveCorrectIsExportAndJavaAccess ()
3014
{
3115
var peer = FindFixtureByJavaName ("my/app/ExportAccessTest");
3216
var publicMethod = peer.MarshalMethods.First (m => m.JniName == "publicMethod");
3317
var protectedMethod = peer.MarshalMethods.First (m => m.JniName == "protectedMethod");
18+
Assert.True (publicMethod.IsExport);
3419
Assert.Equal ("public", publicMethod.JavaAccess);
20+
Assert.True (protectedMethod.IsExport);
3521
Assert.Equal ("protected", protectedMethod.JavaAccess);
3622
}
3723

3824
[Fact]
39-
public void Scanner_RegisterMethod_IsExportFalse ()
25+
public void Scanner_RegisterMethod_IsNotExport ()
4026
{
4127
var peer = FindFixtureByJavaName ("my/app/MixedMethods");
4228
var customMethod = peer.MarshalMethods.First (m => m.JniName == "customMethod");
@@ -45,18 +31,14 @@ public void Scanner_RegisterMethod_IsExportFalse ()
4531
}
4632

4733
[Fact]
48-
public void JcwGenerator_ProtectedExport_UsesProtectedAccess ()
34+
public void JcwGenerator_ExportMethods_UseCorrectAccessModifiers ()
4935
{
5036
var peer = FindFixtureByJavaName ("my/app/ExportAccessTest");
51-
var java = GenerateToString (peer);
37+
var generator = new JcwJavaSourceGenerator ();
38+
using var writer = new StringWriter ();
39+
generator.Generate (peer, writer);
40+
var java = writer.ToString ();
5241
Assert.Contains ("protected void protectedMethod ()", java);
53-
}
54-
55-
[Fact]
56-
public void JcwGenerator_PublicExport_UsesPublicAccess ()
57-
{
58-
var peer = FindFixtureByJavaName ("my/app/ExportAccessTest");
59-
var java = GenerateToString (peer);
6042
Assert.Contains ("public void publicMethod ()", java);
6143
}
6244
}

0 commit comments

Comments
 (0)