From f869756fedf8fbc651dfb6a261d0ba25cf97c03d Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 9 Nov 2020 21:29:13 +0100 Subject: [PATCH] Use "record" instead of "class" for C# 9 record class types. --- .../CSharp/CSharpDecompiler.cs | 4 +-- .../CSharp/OutputVisitor/CSharpAmbience.cs | 3 ++ .../OutputVisitor/CSharpOutputVisitor.cs | 4 +++ .../Syntax/GeneralScope/TypeDeclaration.cs | 8 ++++- ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs | 2 +- .../CSharp/Syntax/TypeSystemAstBuilder.cs | 9 +++++ ICSharpCode.Decompiler/DecompilerSettings.cs | 21 ++++++++++- .../TypeSystem/ITypeDefinition.cs | 5 +++ .../Implementation/MetadataTypeDefinition.cs | 36 +++++++++++++++++-- .../Implementation/MinimalCorlib.cs | 2 ++ .../CSharpHighlightingTokenWriter.cs | 2 +- ILSpy/Properties/Resources.Designer.cs | 9 +++++ ILSpy/Properties/Resources.resx | 3 ++ 13 files changed, 98 insertions(+), 10 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index b3bc01da1..5165df33f 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -18,14 +18,11 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -using System.Runtime.InteropServices; using System.Threading; using ICSharpCode.Decompiler.CSharp.OutputVisitor; @@ -415,6 +412,7 @@ namespace ICSharpCode.Decompiler.CSharp typeSystemAstBuilder.AddResolveResultAnnotations = true; typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables; typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors; + typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses; return typeSystemAstBuilder; } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs index c966c2c87..f24f57489 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs @@ -80,6 +80,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor case ClassType.Enum: writer.WriteKeyword(Roles.EnumKeyword, "enum"); break; + case ClassType.RecordClass: + writer.WriteKeyword(Roles.RecordKeyword, "record"); + break; default: throw new Exception("Invalid value for ClassType"); } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index c8bb5da81..7c3591f52 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1480,6 +1480,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor WriteKeyword(Roles.StructKeyword); braceStyle = policy.StructBraceStyle; break; + case ClassType.RecordClass: + WriteKeyword(Roles.RecordKeyword); + braceStyle = policy.ClassBraceStyle; + break; default: WriteKeyword(Roles.ClassKeyword); braceStyle = policy.ClassBraceStyle; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs index cdc68010f..6a5bdfc75 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs @@ -33,7 +33,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax Class, Struct, Interface, - Enum + Enum, + /// + /// C# 9 'record' + /// + RecordClass, } /// @@ -63,6 +67,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return GetChildByRole(Roles.InterfaceKeyword); case ClassType.Enum: return GetChildByRole(Roles.EnumKeyword); + case ClassType.RecordClass: + return GetChildByRole(Roles.RecordKeyword); default: return CSharpTokenNode.Null; } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index a3f9e69ab..10c9ab72c 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -87,7 +87,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly TokenRole InterfaceKeyword = new TokenRole("interface"); public static readonly TokenRole StructKeyword = new TokenRole("struct"); public static readonly TokenRole ClassKeyword = new TokenRole("class"); + public static readonly TokenRole RecordKeyword = new TokenRole("record"); } } - diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index ab385329f..2dad80d35 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -213,6 +213,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// If disabled, emits "set /*init*/;" instead. /// public bool SupportInitAccessors { get; set; } + + /// + /// Controls whether C# 9 "record" class types are supported. + /// + public bool SupportRecordClasses { get; set; } #endregion #region Convert Type @@ -1744,6 +1749,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } default: classType = ClassType.Class; + if (SupportRecordClasses && typeDefinition.IsRecord) + { + classType = ClassType.RecordClass; + } break; } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index b7e714035..b237678b4 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -135,12 +135,13 @@ namespace ICSharpCode.Decompiler initAccessors = false; functionPointers = false; forEachWithGetEnumeratorExtension = false; + recordClasses = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension) + if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension || recordClasses) return CSharp.LanguageVersion.Preview; if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions || ranges || switchExpressions) @@ -206,6 +207,24 @@ namespace ICSharpCode.Decompiler } } + bool recordClasses = true; + + /// + /// Use C# 9 init; property accessors. + /// + [Category("C# 9.0 (experimental)")] + [Description("DecompilerSettings.RecordClasses")] + public bool RecordClasses { + get { return recordClasses; } + set { + if (recordClasses != value) + { + recordClasses = value; + OnPropertyChanged(); + } + } + } + bool functionPointers = true; /// diff --git a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs index e0e796cc9..4eb6b922c 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs @@ -73,5 +73,10 @@ namespace ICSharpCode.Decompiler.TypeSystem /// This serves as default nullability for members of the type that do not have a [Nullable] attribute. /// Nullability NullableContext { get; } + + /// + /// Gets whether the type has the necessary members to be considered a C# 9 record type. + /// + bool IsRecord { get; } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index 4abfd390b..a6c41404e 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -24,11 +24,8 @@ using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; -using System.Text; -using System.Threading; using ICSharpCode.Decompiler.Metadata; -using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -738,5 +735,38 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return false; } #endregion + + #region IsRecord + byte isRecord = ThreeState.Unknown; + + public bool IsRecord { + get { + if (isRecord == ThreeState.Unknown) + { + isRecord = ThreeState.From(ComputeIsRecord()); + } + return isRecord == ThreeState.True; + } + } + + private bool ComputeIsRecord() + { + if (Kind != TypeKind.Class) + return false; + var metadata = module.metadata; + var typeDef = metadata.GetTypeDefinition(handle); + bool opEquality = false; + bool opInequality = false; + bool clone = false; + foreach (var methodHandle in typeDef.GetMethods()) + { + var method = metadata.GetMethodDefinition(methodHandle); + opEquality |= metadata.StringComparer.Equals(method.Name, "op_Equality"); + opInequality |= metadata.StringComparer.Equals(method.Name, "op_Inequality"); + clone |= metadata.StringComparer.Equals(method.Name, "$"); + } + return opEquality & opInequality & clone; + } + #endregion } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs index 376f5c5f5..6db846f4d 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs @@ -302,6 +302,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return EmptyList.Instance; } + bool ITypeDefinition.IsRecord => false; + ITypeDefinition IType.GetDefinition() => this; TypeParameterSubstitution IType.GetSubstitution() => TypeParameterSubstitution.Identity; diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index 20a480da8..bc1951943 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.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; @@ -228,6 +227,7 @@ namespace ICSharpCode.ILSpy case "class": case "interface": case "delegate": + case "record": color = referenceTypeKeywordsColor; break; case "select": diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index c46b294d8..84d0e8fe3 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1064,6 +1064,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Records. + /// + public static string DecompilerSettings_RecordClasses { + get { + return ResourceManager.GetString("DecompilerSettings.RecordClasses", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove dead and side effect free code (use with caution!). /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index be0899451..8d1638cdc 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -384,6 +384,9 @@ Are you sure you want to continue? Read-only methods + + Records + Remove dead and side effect free code (use with caution!)