From d2488673026f8b75f5b5474de425135353a7df7c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 4 May 2022 13:41:01 +0200 Subject: [PATCH] Add support for C# 10 record structs. --- .../TestCases/Pretty/Records.cs | 269 ++++++++++++------ .../CSharp/CSharpDecompiler.cs | 9 +- .../CSharp/OutputVisitor/CSharpAmbience.cs | 1 + .../OutputVisitor/CSharpOutputVisitor.cs | 7 +- .../CSharp/RecordDecompiler.cs | 6 + .../Syntax/GeneralScope/TypeDeclaration.cs | 7 +- ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs | 1 + .../CSharp/Syntax/TypeSystemAstBuilder.cs | 9 + ...ransformFieldAndConstructorInitializers.cs | 114 ++++---- ICSharpCode.Decompiler/DecompilerSettings.cs | 21 +- ...ransformCollectionAndObjectInitializers.cs | 7 + ICSharpCode.Decompiler/Output/IAmbience.cs | 4 + .../TypeSystem/ITypeDefinition.cs | 2 +- .../Implementation/MetadataTypeDefinition.cs | 4 +- .../CSharpHighlightingTokenWriter.cs | 4 +- ILSpy/Languages/CSharpLanguage.cs | 4 + 16 files changed, 323 insertions(+), 146 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index 82c38d720..30c0402da 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -1,126 +1,231 @@ using System; +using System.Runtime.InteropServices; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { - public record Base(string A); - - public record CopyCtor(string A) + internal class RecordClasses { - protected CopyCtor(CopyCtor _) + public record Base(string A); + + public record CopyCtor(string A) { + protected CopyCtor(CopyCtor _) + { + } } - } - public record Derived(int B) : Base(B.ToString()); + public record Derived(int B) : Base(B.ToString()); - public record Empty; + public record Empty; - public record Fields - { - public int A; - public double B = 1.0; - public object C; - public dynamic D; - public string S = "abc"; - } + public record Fields + { + public int A; + public double B = 1.0; + public object C; + public dynamic D; + public string S = "abc"; + } - public record Interface(int B) : IRecord; + public record Interface(int B) : IRecord; - public interface IRecord - { - } + public interface IRecord + { + } - public record Pair - { - public A First { get; init; } - public B Second { get; init; } - } + public record Pair + { + public A First { get; init; } + public B Second { get; init; } + } - public record PairWithPrimaryCtor(A First, B Second); + public record PairWithPrimaryCtor(A First, B Second); - public record PrimaryCtor(int A, string B); - public record PrimaryCtorWithAttribute([RecordTest("param")] [property: RecordTest("property")][field: RecordTest("field")] int a); - public record PrimaryCtorWithField(int A, string B) - { - public double C = 1.0; - public string D = A + B; - } - public record PrimaryCtorWithInParameter(in int A, in string B); - public record PrimaryCtorWithProperty(int A, string B) - { - public double C { get; init; } = 1.0; - public string D { get; } = A + B; - } + public record PrimaryCtor(int A, string B); + public record PrimaryCtorWithAttribute([RecordTest("param")] [property: RecordTest("property")][field: RecordTest("field")] int a); + public record PrimaryCtorWithField(int A, string B) + { + public double C = 1.0; + public string D = A + B; + } + public record PrimaryCtorWithInParameter(in int A, in string B); + public record PrimaryCtorWithProperty(int A, string B) + { + public double C { get; init; } = 1.0; + public string D { get; } = A + B; + } - public record Properties - { - public int A { get; set; } - public int B { get; } - public int C => 43; - public object O { get; set; } - public string S { get; set; } - public dynamic D { get; set; } + public record Properties + { + public int A { get; set; } + public int B { get; } + public int C => 43; + public object O { get; set; } + public string S { get; set; } + public dynamic D { get; set; } + + public Properties() + { + B = 42; + } + } - public Properties() + [AttributeUsage(AttributeTargets.All)] + public class RecordTestAttribute : Attribute { - B = 42; + public RecordTestAttribute(string name) + { + } } - } - [AttributeUsage(AttributeTargets.All)] - public class RecordTestAttribute : Attribute - { - public RecordTestAttribute(string name) + public sealed record Sealed(string A); + + public sealed record SealedDerived(int B) : Base(B.ToString()); + + public class WithExpressionTests { + public Fields Test(Fields input) + { + return input with { + A = 42, + B = 3.141, + C = input + }; + } + public Fields Test2(Fields input) + { + return input with { + A = 42, + B = 3.141, + C = input with { + A = 43 + } + }; + } } - } - public sealed record Sealed(string A); + public abstract record WithNestedRecords + { + public record A : WithNestedRecords + { + public override string AbstractProp => "A"; + } + + public record B : WithNestedRecords + { + public override string AbstractProp => "B"; + + public int? Value { get; set; } + } - public sealed record SealedDerived(int B) : Base(B.ToString()); + public record DerivedGeneric : Pair where T : struct + { + public bool Flag; + } - public class WithExpressionTests + public abstract string AbstractProp { get; } + } + + } + internal class RecordStructs { - public Fields Test(Fields input) + public record struct Base(string A); + + public record CopyCtor(string A) + { + protected CopyCtor(CopyCtor _) + { + } + } + + [StructLayout(LayoutKind.Sequential, Size = 1)] + public record struct Empty; + + public record struct Fields { - return input with { - A = 42, - B = 3.141, - C = input - }; + public int A; + public double B; + public object C; + public dynamic D; + public string S; } - public Fields Test2(Fields input) + + public record struct Interface(int B) : IRecord; + + public interface IRecord { - return input with { - A = 42, - B = 3.141, - C = input with { - A = 43 - } - }; } - } - public abstract record WithNestedRecords - { - public record A : WithNestedRecords + public record struct Pair { - public override string AbstractProp => "A"; + public A First { get; init; } + public B Second { get; init; } } - public record B : WithNestedRecords + public record struct PairWithPrimaryCtor(A First, B Second); + + public record struct PrimaryCtor(int A, string B); + public record struct PrimaryCtorWithAttribute([RecordTest("param")] [property: RecordTest("property")][field: RecordTest("field")] int a); + public record struct PrimaryCtorWithField(int A, string B) { - public override string AbstractProp => "B"; + public double C = 1.0; + public string D = A + B; + } + public record struct PrimaryCtorWithInParameter(in int A, in string B); + public record struct PrimaryCtorWithProperty(int A, string B) + { + public double C { get; init; } = 1.0; + public string D { get; } = A + B; + } - public int? Value { get; set; } + public record struct Properties + { + public int A { get; set; } + public int B { get; } + public int C => 43; + public object O { get; set; } + public string S { get; set; } + public dynamic D { get; set; } + + public Properties() + { + A = 41; + B = 42; + O = null; + S = "Hello"; + D = null; + } } - public record DerivedGeneric : Pair where T : struct + [AttributeUsage(AttributeTargets.All)] + public class RecordTestAttribute : Attribute { - public bool Flag; + public RecordTestAttribute(string name) + { + } } - public abstract string AbstractProp { get; } + public class WithExpressionTests + { + public Fields Test(Fields input) + { + return input with { + A = 42, + B = 3.141, + C = input + }; + } + public Fields Test2(Fields input) + { + return input with { + A = 42, + B = 3.141, + C = input with { + A = 43 + } + }; + } + } } } namespace System.Runtime.CompilerServices diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 52078eb82..0e9b6ad54 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -505,6 +505,7 @@ namespace ICSharpCode.Decompiler.CSharp typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables; typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors; typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses; + typeSystemAstBuilder.SupportRecordStructs = settings.RecordStructs; return typeSystemAstBuilder; } @@ -1264,7 +1265,11 @@ namespace ICSharpCode.Decompiler.CSharp // e.g. DelegateDeclaration return entityDecl; } - bool isRecord = settings.RecordClasses && typeDef.IsRecord; + bool isRecord = typeDef.Kind switch { + TypeKind.Class => settings.RecordClasses && typeDef.IsRecord, + TypeKind.Struct => settings.RecordStructs && typeDef.IsRecord, + _ => false, + }; RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null; if (recordDecompiler != null) decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler); @@ -1311,7 +1316,7 @@ namespace ICSharpCode.Decompiler.CSharp IEnumerable allOrderedMembers = RequiresNativeOrdering(typeDef) ? GetMembersWithNativeOrdering(typeDef) : fieldsAndProperties.Concat(typeDef.Events).Concat(typeDef.Methods); - var allOrderedEntities = typeDef.NestedTypes.Concat(allOrderedMembers); + var allOrderedEntities = typeDef.NestedTypes.Concat(allOrderedMembers).ToArray(); // Decompile members that are not compiler-generated. foreach (var entity in allOrderedEntities) diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs index 93408ef5f..839fdbb33 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs @@ -232,6 +232,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor astBuilder.UseNullableSpecifierForValueTypes = (ConversionFlags & ConversionFlags.UseNullableSpecifierForValueTypes) != 0; astBuilder.SupportInitAccessors = (ConversionFlags & ConversionFlags.SupportInitAccessors) != 0; astBuilder.SupportRecordClasses = (ConversionFlags & ConversionFlags.SupportRecordClasses) != 0; + astBuilder.SupportRecordStructs = (ConversionFlags & ConversionFlags.SupportRecordStructs) != 0; return astBuilder; } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 30779a0e3..8f1bcc552 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1579,6 +1579,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor WriteKeyword(Roles.RecordKeyword); braceStyle = policy.ClassBraceStyle; break; + case ClassType.RecordStruct: + WriteKeyword(Roles.RecordStructKeyword); + WriteKeyword(Roles.StructKeyword); + braceStyle = policy.StructBraceStyle; + break; default: WriteKeyword(Roles.ClassKeyword); braceStyle = policy.ClassBraceStyle; @@ -1602,7 +1607,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { constraint.AcceptVisitor(this); } - if (typeDeclaration.ClassType == ClassType.RecordClass && typeDeclaration.Members.Count == 0) + if (typeDeclaration.ClassType is (ClassType.RecordClass or ClassType.RecordStruct) && typeDeclaration.Members.Count == 0) { Semicolon(); } diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 82494e34c..7bf416ab0 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -721,6 +721,9 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!body.Instructions[0].MatchReturn(out var returnValue)) return false; + // special case for empty record struct; always returns true; + if (returnValue.MatchLdcI4(1)) + return true; var variables = body.Ancestors.OfType().Single().Variables; var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0); Debug.Assert(IsRecordType(other.Type)); @@ -908,6 +911,9 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!body.Instructions[0].MatchReturn(out var returnValue)) return false; + // special case for empty record struct; always returns false; + if (returnValue.MatchLdcI4(0)) + return true; var hashedMembers = new List(); bool foundBaseClassHash = false; if (!Visit(returnValue)) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs index baec135cf..a60d12843 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs @@ -35,9 +35,13 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax Interface, Enum, /// - /// C# 9 'record' + /// C# 9 'record class' /// RecordClass, + /// + /// C# 10 'record struct' + /// + RecordStruct, } /// @@ -62,6 +66,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax case ClassType.Class: return GetChildByRole(Roles.ClassKeyword); case ClassType.Struct: + case ClassType.RecordStruct: return GetChildByRole(Roles.StructKeyword); case ClassType.Interface: return GetChildByRole(Roles.InterfaceKeyword); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index 076cf9411..88b92c5d5 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -89,6 +89,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly TokenRole StructKeyword = new TokenRole("struct"); public static readonly TokenRole ClassKeyword = new TokenRole("class"); public static readonly TokenRole RecordKeyword = new TokenRole("record"); + public static readonly TokenRole RecordStructKeyword = new TokenRole("record"); } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index d37877ec2..9106e7850 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -219,6 +219,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// Controls whether C# 9 "record" class types are supported. /// public bool SupportRecordClasses { get; set; } + + /// + /// Controls whether C# 10 "record" struct types are supported. + /// + public bool SupportRecordStructs { get; set; } #endregion #region Convert Type @@ -1775,6 +1780,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax modifiers |= Modifiers.Ref; } } + if (SupportRecordStructs && typeDefinition.IsRecord) + { + classType = ClassType.RecordStruct; + } break; case TypeKind.Enum: classType = ClassType.Enum; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index d8d824a98..48ebc81cf 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -61,67 +61,68 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration) { - if (!(constructorDeclaration.Body.Statements.FirstOrDefault() is ExpressionStatement stmt)) - return; var currentCtor = (IMethod)constructorDeclaration.GetSymbol(); - ConstructorInitializer ci; - switch (stmt.Expression) + ConstructorInitializer ci = null; + if (constructorDeclaration.Body.Statements.FirstOrDefault() is ExpressionStatement stmt) { - // Pattern for reference types: - // this..ctor(...); - case InvocationExpression invocation: - if (!(invocation.Target is MemberReferenceExpression mre) || mre.MemberName != ".ctor") - return; - if (!(invocation.GetSymbol() is IMethod ctor && ctor.IsConstructor)) - return; - ci = new ConstructorInitializer(); - var target = mre.Target; - // Ignore casts, those might be added if references are missing. - if (target is CastExpression cast) - target = cast.Expression; - if (target is ThisReferenceExpression or BaseReferenceExpression) - { - if (ctor.DeclaringTypeDefinition == currentCtor.DeclaringTypeDefinition) + switch (stmt.Expression) + { + // Pattern for reference types: + // this..ctor(...); + case InvocationExpression invocation: + if (!(invocation.Target is MemberReferenceExpression mre) || mre.MemberName != ".ctor") + return; + if (!(invocation.GetSymbol() is IMethod ctor && ctor.IsConstructor)) + return; + ci = new ConstructorInitializer(); + var target = mre.Target; + // Ignore casts, those might be added if references are missing. + if (target is CastExpression cast) + target = cast.Expression; + if (target is ThisReferenceExpression or BaseReferenceExpression) + { + if (ctor.DeclaringTypeDefinition == currentCtor.DeclaringTypeDefinition) + ci.ConstructorInitializerType = ConstructorInitializerType.This; + else + ci.ConstructorInitializerType = ConstructorInitializerType.Base; + } + else + return; + // Move arguments from invocation to initializer: + invocation.Arguments.MoveTo(ci.Arguments); + // Add the initializer: (unless it is the default 'base()') + if (!(ci.ConstructorInitializerType == ConstructorInitializerType.Base && ci.Arguments.Count == 0)) + constructorDeclaration.Initializer = ci.CopyAnnotationsFrom(invocation); + // Remove the statement: + stmt.Remove(); + break; + // Pattern for value types: + // this = new TSelf(...); + case AssignmentExpression assignment: + if (!(assignment.Right is ObjectCreateExpression oce && oce.GetSymbol() is IMethod ctor2 && ctor2.DeclaringTypeDefinition == currentCtor.DeclaringTypeDefinition)) + return; + ci = new ConstructorInitializer(); + if (assignment.Left is ThisReferenceExpression) ci.ConstructorInitializerType = ConstructorInitializerType.This; else - ci.ConstructorInitializerType = ConstructorInitializerType.Base; - } - else - return; - // Move arguments from invocation to initializer: - invocation.Arguments.MoveTo(ci.Arguments); - // Add the initializer: (unless it is the default 'base()') - if (!(ci.ConstructorInitializerType == ConstructorInitializerType.Base && ci.Arguments.Count == 0)) - constructorDeclaration.Initializer = ci.CopyAnnotationsFrom(invocation); - // Remove the statement: - stmt.Remove(); - break; - // Pattern for value types: - // this = new TSelf(...); - case AssignmentExpression assignment: - if (!(assignment.Right is ObjectCreateExpression oce && oce.GetSymbol() is IMethod ctor2 && ctor2.DeclaringTypeDefinition == currentCtor.DeclaringTypeDefinition)) - return; - ci = new ConstructorInitializer(); - if (assignment.Left is ThisReferenceExpression) - ci.ConstructorInitializerType = ConstructorInitializerType.This; - else + return; + // Move arguments from invocation to initializer: + oce.Arguments.MoveTo(ci.Arguments); + // Add the initializer: (unless it is the default 'base()') + if (!(ci.ConstructorInitializerType == ConstructorInitializerType.Base && ci.Arguments.Count == 0)) + constructorDeclaration.Initializer = ci.CopyAnnotationsFrom(oce); + // Remove the statement: + stmt.Remove(); + break; + default: return; - // Move arguments from invocation to initializer: - oce.Arguments.MoveTo(ci.Arguments); - // Add the initializer: (unless it is the default 'base()') - if (!(ci.ConstructorInitializerType == ConstructorInitializerType.Base && ci.Arguments.Count == 0)) - constructorDeclaration.Initializer = ci.CopyAnnotationsFrom(oce); - // Remove the statement: - stmt.Remove(); - break; - default: - return; + } } if (context.DecompileRun.RecordDecompilers.TryGetValue(currentCtor.DeclaringTypeDefinition, out var record) - && currentCtor.Equals(record.PrimaryConstructor) - && ci.ConstructorInitializerType == ConstructorInitializerType.Base) + && currentCtor.Equals(record.PrimaryConstructor)) { if (record.IsInheritedRecord && + ci?.ConstructorInitializerType == ConstructorInitializerType.Base && constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl) { var baseType = typeDecl.BaseTypes.First(); @@ -172,7 +173,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (instanceCtorsNotChainingWithThis.Length > 0) { var ctorMethodDef = instanceCtorsNotChainingWithThis[0].GetSymbol() as IMethod; - if (ctorMethodDef != null && ctorMethodDef.DeclaringType.IsReferenceType == false) + ITypeDefinition declaringTypeDefinition = ctorMethodDef?.DeclaringTypeDefinition; + if (ctorMethodDef != null && declaringTypeDefinition?.IsReferenceType == false && !declaringTypeDefinition.IsRecord) return; bool ctorIsUnsafe = instanceCtorsNotChainingWithThis.All(c => c.HasModifier(Modifiers.Unsafe)); @@ -180,13 +182,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (!context.DecompileRun.RecordDecompilers.TryGetValue(ctorMethodDef.DeclaringTypeDefinition, out var record)) record = null; - //Filter out copy constructor of records + // Filter out copy constructor of records if (record != null) instanceCtorsNotChainingWithThis = instanceCtorsNotChainingWithThis.Where(ctor => !record.IsCopyConstructor(ctor.GetSymbol() as IMethod)).ToArray(); // Recognize field or property initializers: // Translate first statement in all ctors (if all ctors have the same statement) into an initializer. bool allSame; + bool isPrimaryCtor = declaringTypeDefinition.IsReferenceType == true && declaringTypeDefinition.IsRecord; do { Match m = fieldInitializerPattern.Match(instanceCtorsNotChainingWithThis[0].Body.FirstOrDefault()); @@ -211,11 +214,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // remove record ctor parameter assignments if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) break; + isPrimaryCtor = true; } else { // cannot transform if member is not found - if (fieldOrPropertyOrEventDecl == null) + if (fieldOrPropertyOrEventDecl == null || !isPrimaryCtor) break; } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 9ae621a4c..e4316ce1f 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -145,6 +145,7 @@ namespace ICSharpCode.Decompiler if (languageVersion < CSharp.LanguageVersion.CSharp10_0) { fileScopedNamespaces = false; + recordStructs = false; } if (languageVersion < CSharp.LanguageVersion.CSharp11_0) { @@ -156,7 +157,7 @@ namespace ICSharpCode.Decompiler { if (parameterNullCheck) return CSharp.LanguageVersion.CSharp11_0; - if (fileScopedNamespaces) + if (fileScopedNamespaces || recordStructs) return CSharp.LanguageVersion.CSharp10_0; if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension || recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns) @@ -262,6 +263,24 @@ namespace ICSharpCode.Decompiler } } + bool recordStructs = true; + + /// + /// Use C# 10 record structs. + /// + [Category("C# 10.0 / VS 2022")] + [Description("DecompilerSettings.RecordStructs")] + public bool RecordStructs { + get { return recordStructs; } + set { + if (recordStructs != value) + { + recordStructs = value; + OnPropertyChanged(); + } + } + } + bool withExpressions = true; /// diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs index 03e5e2176..c77ebf855 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs @@ -105,6 +105,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms initInst = ci.Arguments.Single(); break; default: + var typeDef = v.Type.GetDefinition(); + if (context.Settings.WithExpressions && typeDef?.IsReferenceType == false && typeDef.IsRecord) + { + instType = v.Type; + blockKind = BlockKind.WithInitializer; + break; + } return false; } int initializerItemsCount = 0; diff --git a/ICSharpCode.Decompiler/Output/IAmbience.cs b/ICSharpCode.Decompiler/Output/IAmbience.cs index 4dc853247..d496204b7 100644 --- a/ICSharpCode.Decompiler/Output/IAmbience.cs +++ b/ICSharpCode.Decompiler/Output/IAmbience.cs @@ -105,6 +105,10 @@ namespace ICSharpCode.Decompiler.Output /// Support record classes. /// SupportRecordClasses = 0x20000, + /// + /// Support record structs. + /// + SupportRecordStructs = 0x40000, StandardConversionFlags = ShowParameterNames | ShowAccessibility | diff --git a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs index 8574dd681..2b0672307 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs @@ -88,7 +88,7 @@ namespace ICSharpCode.Decompiler.TypeSystem Nullability NullableContext { get; } /// - /// Gets whether the type has the necessary members to be considered a C# 9 record type. + /// Gets whether the type has the necessary members to be considered a C# 9 record or C# 10 record struct type. /// bool IsRecord { get; } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index e7542643a..433047091 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -758,13 +758,13 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation private bool ComputeIsRecord() { - if (Kind != TypeKind.Class) + if (Kind != TypeKind.Class && Kind != TypeKind.Struct) return false; var metadata = module.metadata; var typeDef = metadata.GetTypeDefinition(handle); bool opEquality = false; bool opInequality = false; - bool clone = false; + bool clone = Kind == TypeKind.Struct; foreach (var methodHandle in typeDef.GetMethods()) { var method = metadata.GetMethodDefinition(methodHandle); diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index ab437bfa4..c3ca4c165 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -231,9 +231,11 @@ namespace ICSharpCode.ILSpy case "class": case "interface": case "delegate": - case "record": color = referenceTypeKeywordsColor; break; + case "record": + color = role == Roles.RecordKeyword ? referenceTypeKeywordsColor : valueTypeKeywordsColor; + break; case "select": case "group": case "by": diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 38e675d4b..abdafa2ec 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -735,6 +735,10 @@ namespace ICSharpCode.ILSpy { flags |= ConversionFlags.SupportRecordClasses; } + if (settings.RecordStructs) + { + flags |= ConversionFlags.SupportRecordStructs; + } if (settings.InitAccessors) { flags |= ConversionFlags.SupportInitAccessors;