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

Commit f8d77fa

Browse files
authored
[generator] Better support deprecated property getter/setters. (#1062)
Fixes: #1033 Context: dotnet/roslyn#32571 When we are converting Java get-method and set-method pairs to C# properties, we can hit an interesting scenario where a getter may be `@Deprecated` and the setter is not, or vice versa: class Example { public boolean hasOptionsMenu () { ... } @deprecated public void setHasOptionsMenu (boolean hasMenu) { ... } } C# has traditionally not allowed `[Obsolete]` to be placed on just a `get` or a `set` block; it could only be on the entire property: partial class Example { [Obsolete] public bool HasOptionsMenu { get; set; } } This can lead to confusion because using the getter will report an obsolete warning when it is not obsolete. Thus, for properties, we only add `[Obsolete]` in 2 cases: 1. The `get` is obsolete and there is no `set` 2. Both the `get` and `set` are obsolete We have this comment in our code: // Unlike [Register], [Obsolete] cannot be put on property accessors, so we can apply them only under limited condition... However, the C# compiler team has determined that preventing `[Obsolete]` on property accessors was a bug, and has fixed it in C#8 via dotnet/roslyn#32571. Update `generator` to support scenarios in which only the Java getter or setter is marked as `@Deprecated`, by placing `[Obsolete]` on the appropriate `get` or `set` block: partial class Example { public bool HasOptionsMenu { get => … [Obsolete] set =>… } }
1 parent 5e6209e commit f8d77fa

3 files changed

Lines changed: 201 additions & 7 deletions

File tree

‎tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs‎

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,8 @@ public void ObsoletedOSPlatformAttributeSupport ()
565565
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a field deprecated since 25!\")]"),writer.ToString());
566566
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a constructor deprecated since 25!\")]"),writer.ToString());
567567
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a method deprecated since 25!\")]"),writer.ToString());
568-
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a property getter deprecated since 25! This is a property setter deprecated since 25!\")]"),writer.ToString());
568+
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a property getter deprecated since 25!\")]"),writer.ToString());
569+
Assert.True(writer.ToString().Contains("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a property setter deprecated since 25!\")]"),writer.ToString());
569570
}
570571

571572
[Test]
@@ -611,7 +612,152 @@ public void ObsoletedOSPlatformAttributeUnneededSupport ()
611612
}
612613

613614
[Test]
614-
[NonParallelizable]// We are setting a static property on Report
615+
publicvoidObsoleteGetterOnlyProperty()
616+
{
617+
varxml=@"<api>
618+
<package name='java.lang' jni-name='java/lang'>
619+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
620+
</package>
621+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
622+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
623+
<method abstract='false' deprecated='deprecated' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public'></method>
624+
</class>
625+
</package>
626+
</api>";
627+
628+
vargens=ParseApiDefinition(xml);
629+
variface=gens.Single(g =>g.Name=="MyClass");
630+
631+
generator.Context.ContextTypes.Push(iface);
632+
generator.WriteType(iface,string.Empty,newGenerationInfo("","","MyAssembly"));
633+
generator.Context.ContextTypes.Pop();
634+
635+
// This should use [Obsolete] on the entire property because the getter is obsolete and there is no setter
636+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete (@\"deprecated\")]public virtual unsafe int Count".NormalizeLineEndings()),writer.ToString());
637+
638+
// Ensure we don't write getter attribute
639+
Assert.False(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"deprecated\")]get"),writer.ToString());
640+
}
641+
642+
[Test]
643+
publicvoidObsoletePropertyGetter()
644+
{
645+
varxml=@"<api>
646+
<package name='java.lang' jni-name='java/lang'>
647+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
648+
</package>
649+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
650+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
651+
<method abstract='false' deprecated='deprecated' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public'></method>
652+
<method abstract='false' deprecated='not deprecated' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public'>
653+
<parameter name='count' type='int' jni-type='I'></parameter>
654+
</method>
655+
</class>
656+
</package>
657+
</api>";
658+
659+
vargens=ParseApiDefinition(xml);
660+
variface=gens.Single(g =>g.Name=="MyClass");
661+
662+
generator.Context.ContextTypes.Push(iface);
663+
generator.WriteType(iface,string.Empty,newGenerationInfo("","","MyAssembly"));
664+
generator.Context.ContextTypes.Pop();
665+
666+
// This should use [Obsolete] on just the property getter since the setter is not obsolete
667+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"deprecated\")]get"),writer.ToString());
668+
}
669+
670+
[Test]
671+
publicvoidObsoletePropertySetter()
672+
{
673+
varxml=@"<api>
674+
<package name='java.lang' jni-name='java/lang'>
675+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
676+
</package>
677+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
678+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
679+
<method abstract='false' deprecated='not deprecated' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public'></method>
680+
<method abstract='false' deprecated='deprecated' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public'>
681+
<parameter name='count' type='int' jni-type='I'></parameter>
682+
</method>
683+
</class>
684+
</package>
685+
</api>";
686+
687+
vargens=ParseApiDefinition(xml);
688+
variface=gens.Single(g =>g.Name=="MyClass");
689+
690+
generator.Context.ContextTypes.Push(iface);
691+
generator.WriteType(iface,string.Empty,newGenerationInfo("","","MyAssembly"));
692+
generator.Context.ContextTypes.Pop();
693+
694+
// This should use [Obsolete] on just the property setter since the getter is not obsolete
695+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"deprecated\")]set"),writer.ToString());
696+
}
697+
698+
[Test]
699+
publicvoidObsoleteBothPropertyMethods()
700+
{
701+
varxml=@"<api>
702+
<package name='java.lang' jni-name='java/lang'>
703+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
704+
</package>
705+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
706+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
707+
<method abstract='false' deprecated='getter_message' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public'></method>
708+
<method abstract='false' deprecated='setter_message' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public'>
709+
<parameter name='count' type='int' jni-type='I'></parameter>
710+
</method>
711+
</class>
712+
</package>
713+
</api>";
714+
715+
vargens=ParseApiDefinition(xml);
716+
variface=gens.Single(g =>g.Name=="MyClass");
717+
718+
generator.Context.ContextTypes.Push(iface);
719+
generator.WriteType(iface,string.Empty,newGenerationInfo("","","MyAssembly"));
720+
generator.Context.ContextTypes.Pop();
721+
722+
// This should use [Obsolete] on both property methods because the deprecation messages are different
723+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"getter_message\")]get"),writer.ToString());
724+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"setter_message\")]set"),writer.ToString());
725+
}
726+
727+
[Test]
728+
publicvoidObsoleteEntireProperty()
729+
{
730+
varxml=@"<api>
731+
<package name='java.lang' jni-name='java/lang'>
732+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
733+
</package>
734+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
735+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
736+
<method abstract='false' deprecated='deprecated' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public'></method>
737+
<method abstract='false' deprecated='deprecated' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public'>
738+
<parameter name='count' type='int' jni-type='I'></parameter>
739+
</method>
740+
</class>
741+
</package>
742+
</api>";
743+
744+
vargens=ParseApiDefinition(xml);
745+
variface=gens.Single(g =>g.Name=="MyClass");
746+
747+
generator.Context.ContextTypes.Push(iface);
748+
generator.WriteType(iface,string.Empty,newGenerationInfo("","","MyAssembly"));
749+
generator.Context.ContextTypes.Pop();
750+
751+
// This should use [Obsolete] on the entire property because the getter and setter are both obsoleted with the same message
752+
Assert.True(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete (@\"deprecated\")]public virtual unsafe int Count".NormalizeLineEndings()),writer.ToString());
753+
754+
// Ensure we don't write getter/setter attributes
755+
Assert.False(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"deprecated\")]get"),writer.ToString());
756+
Assert.False(StripRegisterAttributes(writer.ToString()).NormalizeLineEndings().Contains("[global::System.Obsolete(@\"deprecated\")]set"),writer.ToString());
757+
}
758+
759+
[Test]
760+
[NonParallelizable]// We are setting a static property on Report
615761
publicvoidWarnIfTypeNameMatchesNamespace()
616762
{
617763
var@class=newTestClass("Object","java.myclass.MyClass");
@@ -650,6 +796,20 @@ public void DontWarnIfNestedTypeNameMatchesNamespace ()
650796
// The warning should not be raised if the nested type matches enclosing namespace
651797
Assert.False(sb.ToString().Contains("warning BG8403"));
652798
}
799+
800+
staticstringStripRegisterAttributes(stringstr)
801+
{
802+
// It is hard to test if the [Obsolete] is on the setter/etc due to the [Register], so remove all [Register]s
803+
// [global::System.Obsolete (@"setter_message")]
804+
// [Register ("setCount", "(I)V", "GetSetCount_IHandler")]
805+
// set {
806+
intindex;
807+
808+
while((index=str.IndexOf("[Register",StringComparison.Ordinal))>-1)
809+
str=str.Substring(0,index)+str.Substring(str.IndexOf(']',index)+1);
810+
811+
returnstr;
812+
}
653813
}
654814

655815
[TestFixture]

‎tools/generator/Java.Interop.Tools.Generator.ObjectModel/Property.cs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,32 @@ public void AutoDetectEnumifiedOverrideProperties (AncestorDescendantCache cache
6060
}
6161

6262
publicstringExplicitInterface=>Getter?.ExplicitInterface??Setter?.ExplicitInterface;
63+
64+
publicboolIsWholePropertyDeprecated{
65+
get{
66+
// If the getter isn't deprecated then the property isn't
67+
if(Getter?.Deprecatedisnull)
68+
returnfalse;
69+
70+
// If the getter is deprecated and there is no setter then the property is deprecated
71+
if(Setterisnull)
72+
returntrue;
73+
74+
// If the setter isn't deprecated then the property isn't
75+
if(Setter.Deprecatedisnull)
76+
returnfalse;
77+
78+
// If the getter/setter deprecation messages differ, don't use whole property deprecation
79+
if(Getter.Deprecated!=Setter.Deprecated)
80+
returnfalse;
81+
82+
// If the getter/setter deprecation versions differ, don't use whole property deprecation
83+
if(Getter.DeprecatedSince!=Setter.DeprecatedSince)
84+
returnfalse;
85+
86+
// Getter/Setter deprecation is the same, use whole property deprecation
87+
returntrue;
88+
}
89+
}
6390
}
6491
}

‎tools/generator/SourceWriters/BoundProperty.cs‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ public BoundProperty (GenBase gen, Property property, CodeGenerationOptions opt,
7272
IsOverride=false;
7373
}
7474

75-
// Unlike [Register], [Obsolete] cannot be put on property accessors, so we can apply them only under limited condition...
76-
if(property.Getter.Deprecated!=null&&(property.Setter==null||property.Setter.Deprecated!=null)){
77-
varmessage=property.Getter.Deprecated.Trim()+(property.Setter!=null&&property.Setter.Deprecated!=property.Getter.Deprecated?" "+property.Setter.Deprecated.Trim():null);
78-
varsince=property.Getter?.DeprecatedSince??property.Setter?.DeprecatedSince;
79-
SourceWriterExtensions.AddObsolete(Attributes,message,opt,deprecatedSince:since);
75+
// Add [Obsolete] or [ObsoletedOSPlatform]
76+
if(property.IsWholePropertyDeprecated){
77+
// This case applies [Obsolete] to the entire property
78+
SourceWriterExtensions.AddObsolete(Attributes,property.Getter.Deprecated.Trim(),opt,deprecatedSince:property.Getter.DeprecatedSince);
79+
}else{
80+
// This case applies [Obsolete] to just the getter
81+
if(property.Getter?.Deprecated!=null)
82+
SourceWriterExtensions.AddObsolete(GetterAttributes,property.Getter.Deprecated.Trim(),opt,deprecatedSince:property.Getter?.DeprecatedSince);
83+
84+
// This case applies [Obsolete] to just the setter
85+
if(property.Setter?.Deprecated!=null)
86+
SourceWriterExtensions.AddObsolete(SetterAttributes,property.Setter.Deprecated.Trim(),opt,deprecatedSince:property.Setter?.DeprecatedSince);
8087
}
8188

8289
SourceWriterExtensions.AddSupportedOSPlatform(Attributes,property.Getter,opt);

0 commit comments

Comments
 (0)