Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

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

6 files changed

Lines changed: 96 additions & 19 deletions

File tree

‎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
publicoverridestring?ToString()=>Name;
321+
322+
publicboolIsInternalVisibility=>(Flags&KotlinPropertyFlags.VisibilityMask)==KotlinPropertyFlags.Internal;
323+
324+
publicboolIsPrivateVisibility=>(Flags&KotlinPropertyFlags.VisibilityMask)==KotlinPropertyFlags.Private;
321325
}
322326

323327
publicclassKotlinType
@@ -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(getterisnull&&setterisnull)
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
staticMethodInfo?FindJavaPropertyGetter(KotlinFilekotlinClass,KotlinPropertyproperty,ClassFileklass)
386386
{
387-
varpossible_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+
returnnull;
390+
391+
// Public/protected getters look like "getFoo"
392+
// Internal getters look like "getFoo$main"
393+
varpossible_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
staticMethodInfo?FindJavaPropertySetter(KotlinFilekotlinClass,KotlinPropertyproperty,ClassFileklass)
396406
{
397-
varpossible_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+
returnnull;
410+
411+
// Public/protected setters look like "setFoo"
412+
// Internal setters look like "setFoo$main"
413+
varpossible_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
returnpossible_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
usingSystem;
22
usingSystem.Collections.Generic;
3+
usingSystem.Diagnostics.CodeAnalysis;
34
usingSystem.Linq;
45
usingSystem.Text;
56
usingSystem.Threading.Tasks;
@@ -8,6 +9,18 @@ namespace Xamarin.Android.Tools.Bytecode
89
{
910
publicstaticclassKotlinUtilities
1011
{
12+
[return:NotNullIfNotNull(nameof(value))]
13+
publicstaticstring?Capitalize(thisstring?value)
14+
{
15+
if(string.IsNullOrWhiteSpace(value))
16+
returnvalue;
17+
18+
if(value.Length<1)
19+
returnvalue;
20+
21+
returnchar.ToUpperInvariant(value[0])+value.Substring(1);
22+
}
23+
1124
publicstaticstringConvertKotlinTypeSignature(KotlinType?type,KotlinFile?metadata=null,boolconvertUnsignedToPrimitive=true)
1225
{
1326
if(typeisnull)

‎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
privatevar hitCount =0
99
funhitCount(): Int= hitCount
1010

11-
//Property and setter
11+
//Private property and explicit getter/setter
1212
privatevar type =0
13-
funsetType(type:Int) = { println (type); }
13+
fungetType(): Int= type
14+
funsetType(type:Int) { }
15+
16+
// Private immutable property and explicit getter/setter
17+
privateval type2 =0
18+
fungetType2(): Int= type2
19+
funsetType2(type:Int) { }
20+
21+
// Internal property and explicit getter/setter
22+
internalvar itype =0
23+
fungetItype(): Int= itype
24+
funsetItype(type:Int) { }
25+
26+
// Internal immutable property and explicit getter/setter
27+
internalval itype2 =0
28+
fungetItype2(): Int= itype2
29+
funsetItype2(type:Int) { }
1430
}

0 commit comments

Comments
 (0)