diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index d4b62f34d..691344dab 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -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)
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/SemiAutoProperties.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/SemiAutoProperties.cs
new file mode 100644
index 000000000..21cbb2d08
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/SemiAutoProperties.cs
@@ -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()
+ {
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
index 9ce9ea72b..403b2609f 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
@@ -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
return null;
}
+ ///
+ /// 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.
+ ///
+ 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()
+ .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()
+ .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 attributeSections)
{
RemoveCompilerGeneratedAttribute(attributeSections, "System.Runtime.CompilerServices.CompilerGeneratedAttribute");
@@ -743,15 +794,26 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var parent = identifier.Parent;
var mrr = parent.Annotation();
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();
- 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();
+ parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
+ return Identifier.Create(property.Name);
+ }
}
}
return null;
diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs
index 79055229d..b6a8df47a 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -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
}
}
+ bool semiAutoProperties = true;
+
+ ///
+ /// Gets/Sets whether C# 14.0 semi-auto properties using the field keyword should be used.
+ ///
+ [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;
///
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index 9b6b5b0d5..d70aac931 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -1361,6 +1361,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Use 'field' keyword in semi-auto properties.
+ ///
+ public static string DecompilerSettings_SemiAutoProperties {
+ get {
+ return ResourceManager.GetString("DecompilerSettings.SemiAutoProperties", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Separate local variable declarations and initializers (int x = 5; -> int x; x = 5;), if possible.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 3ce2becb8..859783cf8 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -474,6 +474,9 @@ Are you sure you want to continue?
'scoped' lifetime annotation
+
+ Use 'field' keyword in semi-auto properties
+
Separate local variable declarations and initializers (int x = 5; -> int x; x = 5;), if possible