Skip to content

Commit b4c2a67

Browse files
jpobstjonpryor
authored andcommitted
[Xamarin.Android.Tools.Bytecode] Hide Kotlin synthetic constructors (#700)
Fixes: #694 Context: dotnet/android#3776 When using a Kotlin default constructor like: class MaterialDialog( val windowContext: Context, val dialogBehavior: DialogBehavior = DEFAULT_BEHAVIOR ) : Dialog(windowContext, inferTheme(windowContext, dialogBehavior)) { ... } Kotlin will create 2 constructors, the "real" one, and a synthetic one denoting which constructor is the default constructor using a `kotlin.jvm.internal.DefaultConstructorMarker` parameter: <constructor deprecated="not deprecated" final="false" name="MaterialDialog" static="false" visibility="public" bridge="false" synthetic="false" jni-signature="(Landroid/content/Context;Lcom/afollestad/materialdialogs/DialogBehavior;)V"> <parameter name="windowContext" type="android.content.Context" jni-type="Landroid/content/Context;" not-null="true"/> <parameter name="dialogBehavior" type="com.afollestad.materialdialogs.DialogBehavior" jni-type="Lcom/afollestad/materialdialogs/DialogBehavior;" not-null="true"/> </constructor> <constructor deprecated="not deprecated" final="false" name="MaterialDialog" static="false" visibility="public" bridge="false" synthetic="true" jni-signature="(Landroid/content/Context;Lcom/afollestad/materialdialogs/DialogBehavior;ILkotlin/jvm/internal/DefaultConstructorMarker;)V"> <parameter name="p0" type="android.content.Context" jni-type="Landroid/content/Context;"/> <parameter name="p1" type="com.afollestad.materialdialogs.DialogBehavior" jni-type="Lcom/afollestad/materialdialogs/DialogBehavior;"/> <parameter name="p2" type="int" jni-type="I"/> <parameter name="p3" type="kotlin.jvm.internal.DefaultConstructorMarker" jni-type="Lkotlin/jvm/internal/DefaultConstructorMarker;"/> </constructor> Additionally, the `kotlin.jvm.internal.DefaultConstructorMarker` type is not available in the `Xamarin.Kotlin.StdLib` NuGet package, because the type is `internal`. Consequently, when trying to bind the constructor, `ApiXmlAdjuster` reports this "error": Error while processing '[Constructor] MaterialDialog(android.content.Context p0, com.afollestad.materialdialogs.DialogBehavior p1, int p2, kotlin.jvm.internal.DefaultConstructorMarker p3)' in '[Class] com.afollestad.materialdialogs.MaterialDialog': Type 'kotlin.jvm.internal.DefaultConstructorMarker' was not found. This is actually good, as we shouldn't bind this synthetic constructor, but we should detect this Kotlin-ism and not emit the "error", as it misleads users into thinking they need to do something to fix it. Update `Xamarin.Android.Tools.Bytecode` to *hide* constructors for which the final two parameter types are `int, DefaultConstructorMarker`, thus ensuring that `ApiXmlAdjuster` & co. won't process such members or attempt to resolve the unresolvable `DefaultConstructorMarker` type.
1 parent 4141d84 commit b4c2a67

File tree

6 files changed

+54
-38
lines changed

6 files changed

+54
-38
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ static void FixupJavaMethods (Methods methods)
9393
method.AccessFlags = MethodAccessFlags.Private;
9494
}
9595

96+
// Hide constructor if it's the synthetic DefaultConstructorMarker one
97+
foreach (var method in methods.Where (method => method.IsDefaultConstructorMarker ())) {
98+
Log.Debug ($"Kotlin: Hiding synthetic default constructor in class '{method.DeclaringType?.ThisClass.Name.Value}' with signature '{method.Descriptor}'");
99+
method.AccessFlags = ((method.AccessFlags ^ MethodAccessFlags.Public) & method.AccessFlags) | MethodAccessFlags.Private;
100+
}
101+
96102
// Better parameter names in extension methods
97103
foreach (var method in methods.Where (m => m.IsPubliclyVisible && m.AccessFlags.HasFlag (MethodAccessFlags.Static)))
98104
FixupExtensionMethod (method);

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

+20
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,26 @@ public static string GetMethodNameWithoutSuffix (this MethodInfo method)
7272
return index >= 0 ? method.Name.Substring (0, index) : method.Name;
7373
}
7474

75+
public static bool IsDefaultConstructorMarker (this MethodInfo method)
76+
{
77+
// A default constructor is synthetic and always has an int and a
78+
// DefaultConstructorMarker as its final 2 parameters.
79+
if (method.Name != "<init>")
80+
return false;
81+
82+
if (!method.AccessFlags.HasFlag (MethodAccessFlags.Synthetic))
83+
return false;
84+
85+
var parameters = method.GetParameters ();
86+
87+
if (parameters.Length < 2)
88+
return false;
89+
90+
// Parameter list ends with `int, DefaultConstructorMarker`.
91+
return parameters [parameters.Length - 2].Type.TypeSignature == "I" &&
92+
parameters [parameters.Length - 1].Type.TypeSignature == "Lkotlin/jvm/internal/DefaultConstructorMarker;";
93+
}
94+
7595
public static bool IsPubliclyVisible (this ClassAccessFlags flags) => flags.HasFlag (ClassAccessFlags.Public) || flags.HasFlag (ClassAccessFlags.Protected);
7696

7797
public static bool IsPubliclyVisible (this KotlinClassVisibility flags) => flags == KotlinClassVisibility.Public || flags == KotlinClassVisibility.Protected;

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

+26
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,32 @@ public void HideInternalConstructor ()
5151
Assert.False (ctor.AccessFlags.HasFlag (MethodAccessFlags.Public));
5252
}
5353

54+
[Test]
55+
public void HideDefaultConstructorMarker ()
56+
{
57+
var klass = LoadClassFile ("DefaultConstructor.class");
58+
59+
// init ()
60+
var ctor_0p = klass.Methods.Single (m => m.Name == "<init>" && m.GetParameters ().Length == 0);
61+
62+
// init (string name)
63+
var ctor_1p = klass.Methods.Single (m => m.Name == "<init>" && m.GetParameters ().Length == 1);
64+
65+
// init (string p0, int p1, DefaultConstructorMarker p2)
66+
var ctor_3p = klass.Methods.Single (m => m.Name == "<init>" && m.GetParameters ().Length == 3);
67+
68+
Assert.True (ctor_3p.AccessFlags.HasFlag (MethodAccessFlags.Public));
69+
70+
KotlinFixups.Fixup (new [] { klass });
71+
72+
// Assert that the normal constructors are still public
73+
Assert.True (ctor_0p.AccessFlags.HasFlag (MethodAccessFlags.Public));
74+
Assert.True (ctor_1p.AccessFlags.HasFlag (MethodAccessFlags.Public));
75+
76+
// Assert that the synthetic "DefaultConstructorMarker" constructor has been marked private
77+
Assert.False (ctor_3p.AccessFlags.HasFlag (MethodAccessFlags.Public));
78+
}
79+
5480
[Test]
5581
public void HideImplementationMethod ()
5682
{

tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/JvmOverloadsConstructor.xml

-38
Original file line numberDiff line numberDiff line change
@@ -75,44 +75,6 @@
7575
type="boolean"
7676
jni-type="Z" />
7777
</constructor>
78-
<constructor
79-
deprecated="not deprecated"
80-
final="false"
81-
name="JvmOverloadsConstructor"
82-
static="false"
83-
visibility="public"
84-
bridge="false"
85-
synthetic="true"
86-
jni-signature="(LJvmOverloadsConstructor;IILjava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V">
87-
<parameter
88-
name="p0"
89-
type="JvmOverloadsConstructor"
90-
jni-type="LJvmOverloadsConstructor;" />
91-
<parameter
92-
name="p1"
93-
type="int"
94-
jni-type="I" />
95-
<parameter
96-
name="p2"
97-
type="int"
98-
jni-type="I" />
99-
<parameter
100-
name="p3"
101-
type="java.lang.String"
102-
jni-type="Ljava/lang/String;" />
103-
<parameter
104-
name="p4"
105-
type="boolean"
106-
jni-type="Z" />
107-
<parameter
108-
name="p5"
109-
type="int"
110-
jni-type="I" />
111-
<parameter
112-
name="p6"
113-
type="kotlin.jvm.internal.DefaultConstructorMarker"
114-
jni-type="Lkotlin/jvm/internal/DefaultConstructorMarker;" />
115-
</constructor>
11678
<constructor
11779
deprecated="not deprecated"
11880
final="false"
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class DefaultConstructor (name: String = "bob") {
2+
}

0 commit comments

Comments
 (0)