From 2043e5dd6fad27340a58fa18ee6488a349fb500b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 3 Aug 2024 20:25:07 +0200 Subject: [PATCH] Add support for C# 12 primary constructors. --- .../TestCases/ILPretty/FSharpLoops_Debug.cs | 13 +--- .../TestCases/ILPretty/FSharpLoops_Release.cs | 14 +--- .../Pretty/ConstructorInitializers.cs | 53 ++++++++++++++ .../CSharp/CSharpDecompiler.cs | 63 +++++++++++------ .../CSharp/RecordDecompiler.cs | 70 +++++++++++++++---- ...ransformFieldAndConstructorInitializers.cs | 53 +++++++++++--- ICSharpCode.Decompiler/DecompilerSettings.cs | 21 +++++- ILSpy/Properties/Resources.Designer.cs | 10 +++ ILSpy/Properties/Resources.resx | 3 + 9 files changed, 235 insertions(+), 65 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs index 584b75b18..62762e5ef 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs @@ -50,24 +50,17 @@ public static class Program [Serializable] [SpecialName] [CompilationMapping(SourceConstructFlags.Closure)] - internal sealed class getSeq_00405 : GeneratedSequenceBase + internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase() { [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int pc; + public int pc = pc; [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int current; - - public getSeq_00405(int pc, int current) - { - this.pc = pc; - this.current = current; - base._002Ector(); - } + public int current = current; public override int GenerateNext(ref IEnumerable next) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs index 7d6c0e14b..e8513b055 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs @@ -1,4 +1,3 @@ - // C:\Users\Siegfried\Documents\Visual Studio 2017\Projects\ConsoleApp13\ConsoleApplication1\bin\Release\ConsoleApplication1.exe // ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // Global type: @@ -51,24 +50,17 @@ public static class Program [Serializable] [SpecialName] [CompilationMapping(SourceConstructFlags.Closure)] - internal sealed class getSeq_00405 : GeneratedSequenceBase + internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase() { [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int pc; + public int pc = pc; [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int current; - - public getSeq_00405(int pc, int current) - { - this.pc = pc; - this.current = current; - base._002Ector(); - } + public int current = current; public override int GenerateNext(ref IEnumerable next) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs index 7b60f3ad4..5b08837a0 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs @@ -80,5 +80,58 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public unsafe static int StaticSizeOf = sizeof(SimpleStruct); public unsafe int SizeOf = sizeof(SimpleStruct); } + + +#if CS120 + public class ClassWithPrimaryCtorUsingGlobalParameter(int a) + { + public void Print() + { + Console.WriteLine(a); + } + } + + public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToField(int a) + { + private readonly int a = a; + + public void Print() + { + Console.WriteLine(a); + } + } + + public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToFieldAndUsedInMethod(int a) + { +#pragma warning disable CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event. + private readonly int _a = a; +#pragma warning restore CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event. + + public void Print() + { + Console.WriteLine(a); + } + } + + public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToProperty(int a) + { + public int A { get; set; } = a; + + public void Print() + { + Console.WriteLine(A); + } + } + + public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToEvent(EventHandler a) + { + public event EventHandler A = a; + + public void Print() + { + Console.WriteLine(this.A); + } + } +#endif } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index b6a56b1ab..520619787 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -337,6 +337,8 @@ namespace ICSharpCode.Decompiler.CSharp { if (settings.AnonymousMethods && IsAnonymousMethodCacheField(field, metadata)) return true; + if (settings.UsePrimaryConstructorSyntaxForNonRecordTypes && IsPrimaryConstructorParameterBackingField(field, metadata)) + return true; if (settings.AutomaticProperties && IsAutomaticPropertyBackingField(field, metadata, out var propertyName)) { if (!settings.GetterOnlyAutomaticProperties && IsGetterOnlyProperty(propertyName)) @@ -390,6 +392,11 @@ namespace ICSharpCode.Decompiler.CSharp return false; } + static bool IsPrimaryConstructorParameterBackingField(SRM.FieldDefinition field, MetadataReader metadata) + { + var name = metadata.GetString(field.Name); + return name.StartsWith("<", StringComparison.Ordinal) && name.EndsWith(">P", StringComparison.Ordinal); + } static bool IsSwitchOnStringCache(SRM.FieldDefinition field, MetadataReader metadata) { @@ -1303,12 +1310,12 @@ namespace ICSharpCode.Decompiler.CSharp // e.g. DelegateDeclaration return entityDecl; } - bool isRecord = typeDef.Kind switch { - TypeKind.Class => settings.RecordClasses && typeDef.IsRecord, - TypeKind.Struct => settings.RecordStructs && typeDef.IsRecord, + bool isRecordLike = typeDef.Kind switch { + TypeKind.Class => (settings.RecordClasses && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes, + TypeKind.Struct => (settings.RecordStructs && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes, _ => false, }; - RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null; + RecordDecompiler recordDecompiler = isRecordLike ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null; if (recordDecompiler != null) decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler); @@ -1318,33 +1325,41 @@ namespace ICSharpCode.Decompiler.CSharp { ParameterDeclaration pd = typeSystemAstBuilder.ConvertParameter(p); (IProperty prop, IField field) = recordDecompiler.GetPropertyInfoByPrimaryConstructorParameter(p); - Syntax.Attribute[] attributes = prop.GetAttributes().Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); - if (attributes.Length > 0) + + if (prop != null) { - var section = new AttributeSection { - AttributeTarget = "property" - }; - section.Attributes.AddRange(attributes); - pd.Attributes.Add(section); + var attributes = prop?.GetAttributes().Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); + if (attributes?.Length > 0) + { + var section = new AttributeSection { + AttributeTarget = "property" + }; + section.Attributes.AddRange(attributes); + pd.Attributes.Add(section); + } } - attributes = field.GetAttributes() - .Where(a => !PatternStatementTransform.attributeTypesToRemoveFromAutoProperties.Contains(a.AttributeType.FullName)) - .Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); - if (attributes.Length > 0) + if (field != null && (recordDecompiler.FieldIsGenerated(field) || typeDef.IsRecord)) { - var section = new AttributeSection { - AttributeTarget = "field" - }; - section.Attributes.AddRange(attributes); - pd.Attributes.Add(section); + var attributes = field.GetAttributes() + .Where(a => !PatternStatementTransform.attributeTypesToRemoveFromAutoProperties.Contains(a.AttributeType.FullName)) + .Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); + if (attributes.Length > 0) + { + var section = new AttributeSection { + AttributeTarget = "field" + }; + section.Attributes.AddRange(attributes); + pd.Attributes.Add(section); + } } typeDecl.PrimaryConstructorParameters.Add(pd); } } // With C# 9 records, the relative order of fields and properties matters: - IEnumerable fieldsAndProperties = recordDecompiler?.FieldsAndProperties - ?? typeDef.Fields.Concat(typeDef.Properties); + IEnumerable fieldsAndProperties = isRecordLike && typeDef.IsRecord + ? recordDecompiler.FieldsAndProperties + : typeDef.Fields.Concat(typeDef.Properties); // For COM interop scenarios, the relative order of virtual functions/properties matters: IEnumerable allOrderedMembers = RequiresNativeOrdering(typeDef) ? GetMembersWithNativeOrdering(typeDef) : @@ -1481,6 +1496,10 @@ namespace ICSharpCode.Decompiler.CSharp { return; } + if (recordDecompiler?.FieldIsGenerated(field) == true) + { + return; + } entityDecl = DoDecompile(field, decompileRun, decompilationContext.WithCurrentMember(field)); entityMap.Add(field, entityDecl); break; diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 15d6a6d34..1ddc179b3 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -43,8 +43,8 @@ namespace ICSharpCode.Decompiler.CSharp readonly IType baseClass; readonly Dictionary backingFieldToAutoProperty = new Dictionary(); readonly Dictionary autoPropertyToBackingField = new Dictionary(); - readonly Dictionary primaryCtorParameterToAutoProperty = new Dictionary(); - readonly Dictionary autoPropertyToPrimaryCtorParameter = new Dictionary(); + readonly Dictionary primaryCtorParameterToAutoPropertyOrBackingField = new Dictionary(); + readonly Dictionary autoPropertyOrBackingFieldToPrimaryCtorParameter = new Dictionary(); public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken) { @@ -78,6 +78,8 @@ namespace ICSharpCode.Decompiler.CSharp bool IsAutoProperty(IProperty p, out IField field) { field = null; + if (p.IsStatic) + return false; if (p.Parameters.Count != 0) return false; if (p.Getter != null) @@ -158,8 +160,18 @@ namespace ICSharpCode.Decompiler.CSharp IMethod DetectPrimaryConstructor() { - if (!settings.UsePrimaryConstructorSyntax) - return null; + if (recordTypeDef.IsRecord) + { + if (!settings.UsePrimaryConstructorSyntax) + return null; + } + else + { + if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes) + return null; + if (isStruct) + return null; + } var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); foreach (var method in recordTypeDef.Methods) @@ -170,8 +182,8 @@ namespace ICSharpCode.Decompiler.CSharp var m = method.Specialize(subst); if (IsPrimaryConstructor(m, method)) return method; - primaryCtorParameterToAutoProperty.Clear(); - autoPropertyToPrimaryCtorParameter.Clear(); + primaryCtorParameterToAutoPropertyOrBackingField.Clear(); + autoPropertyOrBackingFieldToPrimaryCtorParameter.Clear(); } return null; @@ -205,10 +217,18 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!(value.Kind == VariableKind.Parameter && value.Index == i)) return false; - if (!backingFieldToAutoProperty.TryGetValue(field, out var property)) - return false; - primaryCtorParameterToAutoProperty.Add(unspecializedMethod.Parameters[i], property); - autoPropertyToPrimaryCtorParameter.Add(property, unspecializedMethod.Parameters[i]); + IMember backingMember; + if (backingFieldToAutoProperty.TryGetValue(field, out var property)) + { + backingMember = property; + } + else + { + backingMember = field; + } + + primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[i], backingMember); + autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, unspecializedMethod.Parameters[i]); } if (!isStruct) @@ -261,6 +281,9 @@ namespace ICSharpCode.Decompiler.CSharp /// public bool MethodIsGenerated(IMethod method) { + if (!recordTypeDef.IsRecord) + return false; + if (IsCopyConstructor(method)) { return IsGeneratedCopyConstructor(method); @@ -320,6 +343,9 @@ namespace ICSharpCode.Decompiler.CSharp internal bool PropertyIsGenerated(IProperty property) { + if (!recordTypeDef.IsRecord) + return false; + switch (property.Name) { case "EqualityContract" when !isStruct: @@ -329,17 +355,35 @@ namespace ICSharpCode.Decompiler.CSharp } } + internal bool FieldIsGenerated(IField field) + { + if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes) + return false; + + var name = field.Name; + return name.StartsWith("<", StringComparison.Ordinal) + && name.EndsWith(">P", StringComparison.Ordinal) + && field.IsCompilerGenerated(); + } + public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property) { var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); return primaryCtor != null - && autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst)); + && autoPropertyOrBackingFieldToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst)); } internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter) { - var prop = primaryCtorParameterToAutoProperty[parameter]; - return (prop, autoPropertyToBackingField[prop]); + var member = primaryCtorParameterToAutoPropertyOrBackingField[parameter]; + if (member is IField field) + return (null, field); + return ((IProperty)member, autoPropertyToBackingField[(IProperty)member]); + } + + internal IParameter GetPrimaryConstructorParameterFromBackingField(IField field) + { + return autoPropertyOrBackingFieldToPrimaryCtorParameter[field]; } public bool IsCopyConstructor(IMethod method) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index 6d00db35a..8bcf924fa 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -16,7 +16,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -24,7 +23,9 @@ using System.Reflection; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; +using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; using SRM = System.Reflection.Metadata; @@ -37,10 +38,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public class TransformFieldAndConstructorInitializers : DepthFirstAstVisitor, IAstTransform { TransformContext context; + Dictionary fieldToVariableMap; public void Run(AstNode node, TransformContext context) { this.context = context; + this.fieldToVariableMap = new(); try { @@ -56,6 +59,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms finally { this.context = null; + this.fieldToVariableMap = null; } } @@ -131,6 +135,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms newBaseType.BaseType = baseType; ci.Arguments.MoveTo(newBaseType.Arguments); } + if (constructorDeclaration.Parent is TypeDeclaration { PrimaryConstructorParameters: var parameters }) + { + foreach (var (cpd, ppd) in constructorDeclaration.Parameters.Zip(parameters)) + { + ppd.CopyAnnotationsFrom(cpd); + } + } constructorDeclaration.Remove(); } } @@ -203,18 +214,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (fieldOrPropertyOrEventDecl is CustomEventDeclaration) break; - Expression initializer = m.Get("initializer").Single(); // 'this'/'base' cannot be used in initializers if (initializer.DescendantsAndSelf.Any(n => n is ThisReferenceExpression || n is BaseReferenceExpression)) break; - - if (initializer.Annotation()?.Variable.Kind == IL.VariableKind.Parameter) + var v = initializer.Annotation()?.Variable; + if (v?.Kind == IL.VariableKind.Parameter) { // remove record ctor parameter assignments - if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) + if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent, record)) break; isStructPrimaryCtor = true; + if (fieldOrPropertyOrEvent is IField f) + fieldToVariableMap.Add(f, v); } else { @@ -264,11 +276,21 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } } - bool IsPropertyDeclaredByPrimaryCtor(IProperty p, RecordDecompiler record) + bool IsPropertyDeclaredByPrimaryCtor(IMember m, RecordDecompiler record) { - if (p == null || record == null) + if (record == null) return false; - return record.IsPropertyDeclaredByPrimaryConstructor(p); + switch (m) + { + case IProperty p: + return record.IsPropertyDeclaredByPrimaryConstructor(p); + case IField f: + return true; + case IEvent e: + return true; + default: + return false; + } } void RemoveSingleEmptyConstructor(IEnumerable members, ITypeDefinition contextTypeDefinition) @@ -443,5 +465,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return false; } } + + public override void VisitIdentifier(Identifier identifier) + { + if (identifier.Parent?.GetSymbol() is not IField field) + { + return; + } + if (!fieldToVariableMap.TryGetValue(field, out var v)) + { + return; + } + identifier.Parent.RemoveAnnotations(); + identifier.Parent.AddAnnotation(new ILVariableResolveResult(v)); + identifier.ReplaceWith(Identifier.Create(v.Name)); + } } } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index c9d2e77ab..773128bc8 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -162,12 +162,13 @@ namespace ICSharpCode.Decompiler if (languageVersion < CSharp.LanguageVersion.CSharp12_0) { refReadOnlyParameters = false; + usePrimaryConstructorSyntaxForNonRecordTypes = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (refReadOnlyParameters) + if (refReadOnlyParameters || usePrimaryConstructorSyntaxForNonRecordTypes) return CSharp.LanguageVersion.CSharp12_0; if (scopedRef || requiredMembers || numericIntPtr || utf8StringLiterals || unsignedRightShift || checkedOperators) return CSharp.LanguageVersion.CSharp11_0; @@ -2015,6 +2016,24 @@ namespace ICSharpCode.Decompiler } } + bool usePrimaryConstructorSyntaxForNonRecordTypes = true; + + /// + /// Use primary constructor syntax with classes and structs. + /// + [Category("C# 12.0 / VS 2022.8")] + [Description("DecompilerSettings.UsePrimaryConstructorSyntaxForNonRecordTypes")] + public bool UsePrimaryConstructorSyntaxForNonRecordTypes { + get { return usePrimaryConstructorSyntaxForNonRecordTypes; } + set { + if (usePrimaryConstructorSyntaxForNonRecordTypes != value) + { + usePrimaryConstructorSyntaxForNonRecordTypes = value; + OnPropertyChanged(); + } + } + } + bool separateLocalVariableDeclarations = false; /// diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 54ec14ad5..ca8b55151 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1478,6 +1478,16 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Use primary constructor syntax for non-record types. + /// + public static string DecompilerSettings_UsePrimaryConstructorDecompilerSettings_SyntaxForNonRecordTypes { + get { + return ResourceManager.GetString("DecompilerSettings.UsePrimaryConstructorDecompilerSettings.SyntaxForNonRecordType" + + "s", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use primary constructor syntax with records. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 096c9d74b..b853b0641 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -516,6 +516,9 @@ Are you sure you want to continue? Use pattern-based fixed statement + + Use primary constructor syntax for non-record types + Use primary constructor syntax with records