Browse Source

Implement field keyword support for semi-auto properties in C# 14

Co-authored-by: christophwille <344208+christophwille@users.noreply.github.com>
copilot/add-field-keyword-auto-properties
copilot-swe-agent[bot] 1 month ago
parent
commit
750e6a9c4a
  1. 6
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 76
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/SemiAutoProperties.cs
  3. 78
      ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
  4. 21
      ICSharpCode.Decompiler/DecompilerSettings.cs
  5. 9
      ILSpy/Properties/Resources.Designer.cs
  6. 3
      ILSpy/Properties/Resources.resx

6
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -565,6 +565,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -565,6 +565,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview);
}
[Test]
public async Task SemiAutoProperties([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview);
}
[Test]
public async Task NullPropagation([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions)
{

76
ICSharpCode.Decompiler.Tests/TestCases/Pretty/SemiAutoProperties.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class SemiAutoProperties
{
// Semi-auto property with validation in setter
public int ValidatedProperty {
get => field;
set {
if (value < 0)
{
throw new ArgumentOutOfRangeException();
}
field = value;
}
}
// Semi-auto property with lazy initialization
public string LazyProperty {
get {
if (field == null)
{
field = "default";
}
return field;
}
set => field = value;
}
// Semi-auto property with notification
public int NotifyProperty {
get => field;
set {
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
}
// Getter-only semi-auto property with initialization
public int ReadOnlyWithInit {
get {
if (field == 0)
{
field = 42;
}
return field;
}
}
private void OnPropertyChanged()
{
}
}
}

78
ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs

@ -111,7 +111,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -111,7 +111,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (result != null)
return result;
}
return base.VisitPropertyDeclaration(propertyDeclaration);
// First visit children (accessor bodies) before potentially transforming to semi-auto property
var visitedNode = base.VisitPropertyDeclaration(propertyDeclaration);
// After visiting children, check if we should transform to semi-auto property
if (context.Settings.SemiAutoProperties && context.Settings.AutomaticProperties)
{
TransformSemiAutoProperty(propertyDeclaration);
}
return visitedNode;
}
public override AstNode VisitCustomEventDeclaration(CustomEventDeclaration eventDeclaration)
@ -660,6 +667,50 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -660,6 +667,50 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return null;
}
/// <summary>
/// Transforms a property that uses the backing field into a semi-auto property
/// by removing the backing field declaration. The field keyword replacements
/// are already done in ReplaceBackingFieldUsage during child visiting.
/// </summary>
void TransformSemiAutoProperty(PropertyDeclaration propertyDeclaration)
{
IProperty property = propertyDeclaration.GetSymbol() as IProperty;
if (property == null)
return;
// Check if any accessor body contains the 'field' keyword (which was transformed from backing field)
bool usesFieldKeyword = false;
foreach (var accessor in new[] { propertyDeclaration.Getter, propertyDeclaration.Setter })
{
if (accessor.IsNull || accessor.Body.IsNull)
continue;
usesFieldKeyword |= accessor.Body.Descendants.OfType<Identifier>()
.Any(id => id.Name == "field");
}
if (!usesFieldKeyword)
return;
// Find and remove the backing field declaration
string backingFieldName = "<" + property.Name + ">k__BackingField";
var fieldDecl = propertyDeclaration.Parent?.Children.OfType<FieldDeclaration>()
.FirstOrDefault(fd => {
var field = fd.GetSymbol() as IField;
return field != null && field.Name == backingFieldName
&& field.IsCompilerGenerated()
&& field.DeclaringTypeDefinition == property.DeclaringTypeDefinition;
});
if (fieldDecl != null)
{
fieldDecl.Remove();
// Move field attributes to the property with [field: ...] target
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.CompilerGenerated);
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.DebuggerBrowsable);
foreach (var section in fieldDecl.Attributes)
{
section.AttributeTarget = "field";
propertyDeclaration.Attributes.Add(section.Detach());
}
}
}
void RemoveCompilerGeneratedAttribute(AstNodeCollection<AttributeSection> attributeSections)
{
RemoveCompilerGeneratedAttribute(attributeSections, "System.Runtime.CompilerServices.CompilerGeneratedAttribute");
@ -743,15 +794,26 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -743,15 +794,26 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var parent = identifier.Parent;
var mrr = parent.Annotation<MemberResolveResult>();
var field = mrr?.Member as IField;
if (field != null && IsBackingFieldOfAutomaticProperty(field, out var property)
&& CanTransformToAutomaticProperty(property, !(field.IsCompilerGenerated() && field.Name == "_" + property.Name))
&& currentMethod.AccessorOwner != property)
if (field != null && IsBackingFieldOfAutomaticProperty(field, out var property))
{
if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties)
if (currentMethod?.AccessorOwner == property)
{
// We're inside this property's accessor - use the field keyword if enabled
if (context.Settings.SemiAutoProperties)
{
return Identifier.Create("field");
}
return null;
parent.RemoveAnnotations<MemberResolveResult>();
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
return Identifier.Create(property.Name);
}
// Outside of property accessor - check if we can replace with property name
if (CanTransformToAutomaticProperty(property, !(field.IsCompilerGenerated() && field.Name == "_" + property.Name)))
{
if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties)
return null;
parent.RemoveAnnotations<MemberResolveResult>();
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
return Identifier.Create(property.Name);
}
}
}
return null;

21
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -175,12 +175,13 @@ namespace ICSharpCode.Decompiler @@ -175,12 +175,13 @@ namespace ICSharpCode.Decompiler
if (languageVersion < CSharp.LanguageVersion.CSharp14_0)
{
extensionMembers = false;
semiAutoProperties = false;
}
}
public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
if (extensionMembers)
if (extensionMembers || semiAutoProperties)
return CSharp.LanguageVersion.CSharp14_0;
if (paramsCollections)
return CSharp.LanguageVersion.CSharp13_0;
@ -2179,6 +2180,24 @@ namespace ICSharpCode.Decompiler @@ -2179,6 +2180,24 @@ namespace ICSharpCode.Decompiler
}
}
bool semiAutoProperties = true;
/// <summary>
/// Gets/Sets whether C# 14.0 semi-auto properties using the <c>field</c> keyword should be used.
/// </summary>
[Category("C# 14.0 / VS 202x.yy")]
[Description("DecompilerSettings.SemiAutoProperties")]
public bool SemiAutoProperties {
get { return semiAutoProperties; }
set {
if (semiAutoProperties != value)
{
semiAutoProperties = value;
OnPropertyChanged();
}
}
}
bool separateLocalVariableDeclarations = false;
/// <summary>

9
ILSpy/Properties/Resources.Designer.cs generated

@ -1361,6 +1361,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1361,6 +1361,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Use &apos;field&apos; keyword in semi-auto properties.
/// </summary>
public static string DecompilerSettings_SemiAutoProperties {
get {
return ResourceManager.GetString("DecompilerSettings.SemiAutoProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Separate local variable declarations and initializers (int x = 5; -&gt; int x; x = 5;), if possible.
/// </summary>

3
ILSpy/Properties/Resources.resx

@ -474,6 +474,9 @@ Are you sure you want to continue?</value> @@ -474,6 +474,9 @@ Are you sure you want to continue?</value>
<data name="DecompilerSettings.ScopedRef" xml:space="preserve">
<value>'scoped' lifetime annotation</value>
</data>
<data name="DecompilerSettings.SemiAutoProperties" xml:space="preserve">
<value>Use 'field' keyword in semi-auto properties</value>
</data>
<data name="DecompilerSettings.SeparateLocalVariableDeclarations" xml:space="preserve">
<value>Separate local variable declarations and initializers (int x = 5; -&gt; int x; x = 5;), if possible</value>
</data>

Loading…
Cancel
Save