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