Skip to content

Commit 6bd7ae4

Browse files
authored
[Xamarin.Android.Tools.Bytecode] Kotlin internal prop visibility (#1151)
Fixes: #1139 Context: https://jetbrains.gitbooks.io/kotlin-reference-for-kindle/content/properties.html Kotlin does not allow classes to explicitly contain fields: > Classes in Kotlin cannot have fields. They can *implicitly* contain fields, but not explicitly. Syntax that *look like* a field to those who don't know Kotlin: /* partial */ class EmojiPickerView { private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null } are in fact *properties*, which may or may not involve a compiler- generated backing field. When a property is declared in Kotlin, Java `get*` and/or `set*` methods are generated as needed. In the case where the property is `internal` -- which is not supported by Java -- `public` getters or setters are generated. We wish to "hide" these as they are not intended to be part of the public API. That is, they would not be callable by Kotlin code. However, consider these code fragments from [`EmojiPickerView.kt`][0]: /* partial */ class EmojiPickerView { // Line 100 private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null // Lines 317-319 fun setOnEmojiPickedListener(onEmojiPickedListener: Consumer<EmojiViewItem>?) { this.onEmojiPickedListener = onEmojiPickedListener } } Default member visibility in Kotlin is `public`, so this declares a private `onEmojiPickedListener` property and a public `setOnEmojiPickedListener()` method. However, we were incorrectly determining visibility (678c4bd): we treated `onEmojiPickedListener` and `setOnEmojiPickedListener()` as if they were part of the same property. As part of this association, we saw that `onEmojiPickedListener` was private, and marked `setOnEmojiPickedListener()` as private to follow suit. This was incorrect, because `private` properties do not have setters *at all*, so we should not attempt to hide anything for `private` properties, only for `internal` properties. With that incorrect association broken, that allows `setOnEmojiPickedListener()` to be considered separately, and found to have `public` visibility. For example, this Kotlin code: private var type = 0 internal var itype = 0 generates Java code equivalent to: private int type; private int itype; public final int getItype$main() { return this.itype; } public final void setItype$main(int <set-?>) { this.itype = <set-?>; Additionally, when matching `internal` properties to their getters or setters, the generated name differs from the names given to `public` getters and setters. // Kotlin: public var type = 0 // Java: public final int getType () { ... } // Kotlin: internal var type = 0 // Java: public final int getType$main () { ... } Fix this scenario: * Do not attempt to hide getters/setters for `private` Kotlin properties. * Improve matching of generated names for `internal` Kotlin properties. Additionally, our existing unit test in `NameShadowing.kt` was written incorrectly: // Incorrect, return type of a function fun setType(type: Int) = { println (type); } // Correct, return type of void fun setType(type: Int) { println (type); } The fixed `NameShadowing.kt` unit test fails without the other changes here. Additionally, add several more unit test cases to cover `internal` properties and mangled getter/setter names. Additionally, some enum values in `KotlinPropertyFlags` were specified incorrectly which has been fixed. [0]: https://github.com/androidx/androidx/blob/0d655214d339e006f4e13a85f55c78770c885f2e/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
1 parent 3c83179 commit 6bd7ae4

File tree

6 files changed

+96
-19
lines changed

6 files changed

+96
-19
lines changed

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ public class KotlinProperty : KotlinMethodBase
318318
}
319319

320320
public override string? ToString () => Name;
321+
322+
public bool IsInternalVisibility => (Flags & KotlinPropertyFlags.VisibilityMask) == KotlinPropertyFlags.Internal;
323+
324+
public bool IsPrivateVisibility => (Flags & KotlinPropertyFlags.VisibilityMask) == KotlinPropertyFlags.Private;
321325
}
322326

323327
public class KotlinType
@@ -688,6 +692,8 @@ public enum KotlinPropertyFlags
688692
PrivateToThis = 0b00_00_100_0,
689693
Local = 0b00_00_101_0,
690694

695+
VisibilityMask = 0b00_00_111_0,
696+
691697
Final = 0b00_00_000_0,
692698
Open = 0b00_01_000_0,
693699
Abstract = 0b00_10_000_0,
@@ -698,15 +704,15 @@ public enum KotlinPropertyFlags
698704
Delegation = 0b10_00_000_0,
699705
Synthesized = 0b11_00_000_0,
700706

701-
IsVar = 0b_000000001_000_00_000_0,
702-
HasGetter = 0b_000000010_000_00_000_0,
703-
HasSetter = 0b_000000100_000_00_000_0,
704-
IsConst = 0b_000001000_000_00_000_0,
705-
IsLateInit = 0b_000010000_000_00_000_0,
706-
HasConstant = 0b_000100000_000_00_000_0,
707-
IsExternalProperty = 0b_001000000_000_00_000_0,
708-
IsDelegated = 0b_010000000_000_00_000_0,
709-
IsExpectProperty = 0b_100000000_000_00_000_0
707+
IsVar = 0b_000000001_00_00_000_0,
708+
HasGetter = 0b_000000010_00_00_000_0,
709+
HasSetter = 0b_000000100_00_00_000_0,
710+
IsConst = 0b_000001000_00_00_000_0,
711+
IsLateInit = 0b_000010000_00_00_000_0,
712+
HasConstant = 0b_000100000_00_00_000_0,
713+
IsExternalProperty = 0b_001000000_00_00_000_0,
714+
IsDelegated = 0b_010000000_00_00_000_0,
715+
IsExpectProperty = 0b_100000000_00_00_000_0
710716
}
711717

712718
[Flags]

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinPropert
272272
if (getter is null && setter is null)
273273
return;
274274

275-
// Hide property if it isn't Public/Protected
276-
if (!metadata.Flags.IsPubliclyVisible ()) {
275+
// Hide getters/setters if property is Internal
276+
if (metadata.IsInternalVisibility) {
277277

278278
if (getter?.IsPubliclyVisible == true) {
279279
Log.Debug ($"Kotlin: Hiding internal getter method {getter.DeclaringType?.ThisClass.Name.Value} - {getter.Name}");
@@ -384,7 +384,17 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
384384

385385
static MethodInfo? FindJavaPropertyGetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass)
386386
{
387-
var possible_methods = klass.Methods.Where (method => string.Compare (method.GetMethodNameWithoutSuffix (), $"get{property.Name}", StringComparison.OrdinalIgnoreCase) == 0 &&
387+
// Private properties do not have getters
388+
if (property.IsPrivateVisibility)
389+
return null;
390+
391+
// Public/protected getters look like "getFoo"
392+
// Internal getters look like "getFoo$main"
393+
var possible_methods = property.IsInternalVisibility ?
394+
klass.Methods.Where (method => method.Name.StartsWith ($"get{property.Name.Capitalize ()}$", StringComparison.Ordinal)) :
395+
klass.Methods.Where (method => method.Name.Equals ($"get{property.Name.Capitalize ()}", StringComparison.Ordinal));
396+
397+
possible_methods = possible_methods.Where (method =>
388398
method.GetParameters ().Length == 0 &&
389399
property.ReturnType != null &&
390400
TypesMatch (method.ReturnType, property.ReturnType, kotlinClass));
@@ -394,11 +404,21 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
394404

395405
static MethodInfo? FindJavaPropertySetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass)
396406
{
397-
var possible_methods = klass.Methods.Where (method => string.Compare (method.GetMethodNameWithoutSuffix (), $"set{property.Name}", StringComparison.OrdinalIgnoreCase) == 0 &&
398-
property.ReturnType != null &&
399-
method.GetParameters ().Length == 1 &&
400-
method.ReturnType.BinaryName == "V" &&
401-
TypesMatch (method.GetParameters () [0].Type, property.ReturnType, kotlinClass));
407+
// Private properties do not have setters
408+
if (property.IsPrivateVisibility)
409+
return null;
410+
411+
// Public/protected setters look like "setFoo"
412+
// Internal setters look like "setFoo$main"
413+
var possible_methods = property.IsInternalVisibility ?
414+
klass.Methods.Where (method => method.Name.StartsWith ($"set{property.Name.Capitalize ()}$", StringComparison.Ordinal)) :
415+
klass.Methods.Where (method => method.Name.Equals ($"set{property.Name.Capitalize ()}", StringComparison.Ordinal));
416+
417+
possible_methods = possible_methods.Where (method =>
418+
property.ReturnType != null &&
419+
method.GetParameters ().Length == 1 &&
420+
method.ReturnType.BinaryName == "V" &&
421+
TypesMatch (method.GetParameters () [0].Type, property.ReturnType, kotlinClass));
402422

403423
return possible_methods.FirstOrDefault ();
404424
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using System.Text;
56
using System.Threading.Tasks;
@@ -8,6 +9,18 @@ namespace Xamarin.Android.Tools.Bytecode
89
{
910
public static class KotlinUtilities
1011
{
12+
[return: NotNullIfNotNull (nameof (value))]
13+
public static string? Capitalize (this string? value)
14+
{
15+
if (string.IsNullOrWhiteSpace (value))
16+
return value;
17+
18+
if (value.Length < 1)
19+
return value;
20+
21+
return char.ToUpperInvariant (value [0]) + value.Substring (1);
22+
}
23+
1124
public static string ConvertKotlinTypeSignature (KotlinType? type, KotlinFile? metadata = null, bool convertUnsignedToPrimitive = true)
1225
{
1326
if (type is null)

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,29 @@ public void HandleKotlinNameShadowing ()
324324

325325
Assert.True (klass.Methods.Single (m => m.Name == "count").AccessFlags.HasFlag (MethodAccessFlags.Public));
326326
Assert.True (klass.Methods.Single (m => m.Name == "hitCount").AccessFlags.HasFlag (MethodAccessFlags.Public));
327+
328+
// Private property and explicit getter/setter
329+
// There is no generated getter/setter, and explicit ones should be public
330+
Assert.True (klass.Methods.Single (m => m.Name == "getType").AccessFlags.HasFlag (MethodAccessFlags.Public));
327331
Assert.True (klass.Methods.Single (m => m.Name == "setType").AccessFlags.HasFlag (MethodAccessFlags.Public));
332+
333+
// Private immutable property and explicit getter/setter
334+
// There is no generated getter/setter, and explicit ones should be public
335+
Assert.True (klass.Methods.Single (m => m.Name == "getType2").AccessFlags.HasFlag (MethodAccessFlags.Public));
336+
Assert.True (klass.Methods.Single (m => m.Name == "setType2").AccessFlags.HasFlag (MethodAccessFlags.Public));
337+
338+
// Internal property and explicit getter/setter
339+
// Generated getter/setter should not be public, and explicit ones should be public
340+
Assert.False (klass.Methods.Single (m => m.Name == "getItype$main").AccessFlags.HasFlag (MethodAccessFlags.Public));
341+
Assert.False (klass.Methods.Single (m => m.Name == "setItype$main").AccessFlags.HasFlag (MethodAccessFlags.Public));
342+
Assert.True (klass.Methods.Single (m => m.Name == "getItype").AccessFlags.HasFlag (MethodAccessFlags.Public));
343+
Assert.True (klass.Methods.Single (m => m.Name == "setItype").AccessFlags.HasFlag (MethodAccessFlags.Public));
344+
345+
// Internal immutable property and explicit getter/setter
346+
// Generated getter should not be public, and explicit ones should be public
347+
Assert.False (klass.Methods.Single (m => m.Name == "getItype2$main").AccessFlags.HasFlag (MethodAccessFlags.Public));
348+
Assert.True (klass.Methods.Single (m => m.Name == "getItype2").AccessFlags.HasFlag (MethodAccessFlags.Public));
349+
Assert.True (klass.Methods.Single (m => m.Name == "setItype2").AccessFlags.HasFlag (MethodAccessFlags.Public));
328350
}
329351

330352
[Test]
Binary file not shown.

tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin/NameShadowing.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,23 @@ public class NameShadowing {
88
private var hitCount = 0
99
fun hitCount(): Int = hitCount
1010

11-
// Property and setter
11+
// Private property and explicit getter/setter
1212
private var type = 0
13-
fun setType(type: Int) = { println (type); }
13+
fun getType(): Int = type
14+
fun setType(type: Int) { }
15+
16+
// Private immutable property and explicit getter/setter
17+
private val type2 = 0
18+
fun getType2(): Int = type2
19+
fun setType2(type: Int) { }
20+
21+
// Internal property and explicit getter/setter
22+
internal var itype = 0
23+
fun getItype(): Int = itype
24+
fun setItype(type: Int) { }
25+
26+
// Internal immutable property and explicit getter/setter
27+
internal val itype2 = 0
28+
fun getItype2(): Int = itype2
29+
fun setItype2(type: Int) { }
1430
}

0 commit comments

Comments
 (0)