From f869756fedf8fbc651dfb6a261d0ba25cf97c03d Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 9 Nov 2020 21:29:13 +0100 Subject: [PATCH 01/16] 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!) From 9e589faeea02f633e91aba565f24a324fe0a3702 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 9 Nov 2020 21:45:23 +0100 Subject: [PATCH 02/16] Omit members from record definitions if they are always compiler-generated. --- .../CSharp/CSharpDecompiler.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 5165df33f..ff12ddb30 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1168,6 +1168,7 @@ namespace ICSharpCode.Decompiler.CSharp // e.g. DelegateDeclaration return entityDecl; } + bool isRecord = settings.RecordClasses && typeDef.IsRecord; foreach (var type in typeDef.NestedTypes) { if (!type.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, type.MetadataToken, settings)) @@ -1205,6 +1206,31 @@ namespace ICSharpCode.Decompiler.CSharp } foreach (var method in typeDef.Methods) { + if (isRecord) + { + // Some members in records are always compiler-generated and lead to a + // "duplicate definition" error if we emit the generated code. + if ((method.Name == "op_Equality" || method.Name == "op_Inequality") + && method.Parameters.Count == 2 + && method.Parameters.All(p => p.Type.GetDefinition() == typeDef)) + { + // Don't emit comparison operators into C# record definition + // Note: user can declare additional operator== as long as they have + // different parameter types. + continue; + } + if (method.Name == "Equals" && method.Parameters.Count == 1 && method.Parameters[0].Type.IsKnownType(KnownTypeCode.Object)) + { + // override bool Equals(object? obj) + // Note: the "virtual bool Equals(R? other)" overload can be customized + continue; + } + if (method.Name == "$" && method.Parameters.Count == 0) + { + // Method name cannot be expressed in C# + continue; + } + } if (!method.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, method.MetadataToken, settings)) { var memberDecl = DoDecompile(method, decompileRun, decompilationContext.WithCurrentMember(method)); From da4539b15f8de6b713ea5a01aed9d47e041040ca Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 9 Nov 2020 21:51:01 +0100 Subject: [PATCH 03/16] Records: Omit "IEquatable" in base class list --- .../CSharp/Syntax/TypeSystemAstBuilder.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 2dad80d35..08f87fa5a 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1783,22 +1783,30 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax { foreach (IType baseType in typeDefinition.DirectBaseTypes) { - // if the declared type is an enum, replace all references to System.Enum with the enum-underlying type if (typeDefinition.Kind == TypeKind.Enum && baseType.IsKnownType(KnownTypeCode.Enum)) { + // if the declared type is an enum, replace all references to System.Enum with the enum-underlying type if (!typeDefinition.EnumUnderlyingType.IsKnownType(KnownTypeCode.Int32)) { decl.BaseTypes.Add(ConvertType(typeDefinition.EnumUnderlyingType)); } - // if the declared type is a struct, ignore System.ValueType } else if ((typeDefinition.Kind == TypeKind.Struct || typeDefinition.Kind == TypeKind.Void) && baseType.IsKnownType(KnownTypeCode.ValueType)) { + // if the declared type is a struct, ignore System.ValueType continue; - // always ignore System.Object } else if (baseType.IsKnownType(KnownTypeCode.Object)) { + // always ignore System.Object + continue; + } + else if (SupportRecordClasses && typeDefinition.IsRecord + && baseType.Name == "IEquatable" && baseType.Namespace == "System" + && baseType.TypeArguments.Count == 1 + && baseType.TypeArguments[0].Equals(typeDefinition)) + { + // omit "IEquatable" in records continue; } else From 90ce77f40039f6dff4dbd9e342ce3a203a481e09 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 12 Nov 2020 23:27:44 +0100 Subject: [PATCH 04/16] Omit EqualityContract if it's automatically generated. --- .../CSharp/CSharpDecompiler.cs | 30 +--- .../CSharp/RecordDecompiler.cs | 145 ++++++++++++++++++ .../ICSharpCode.Decompiler.csproj | 1 + 3 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index ff12ddb30..9ac8dc2b4 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1169,6 +1169,7 @@ namespace ICSharpCode.Decompiler.CSharp return entityDecl; } bool isRecord = settings.RecordClasses && typeDef.IsRecord; + RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, CancellationToken) : null; foreach (var type in typeDef.NestedTypes) { if (!type.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, type.MetadataToken, settings)) @@ -1190,6 +1191,10 @@ namespace ICSharpCode.Decompiler.CSharp } foreach (var property in typeDef.Properties) { + if (recordDecompiler?.PropertyIsGenerated(property) == true) + { + continue; + } if (!property.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, property.MetadataToken, settings)) { var propDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property)); @@ -1206,30 +1211,9 @@ namespace ICSharpCode.Decompiler.CSharp } foreach (var method in typeDef.Methods) { - if (isRecord) + if (recordDecompiler?.MethodIsGenerated(method) == true) { - // Some members in records are always compiler-generated and lead to a - // "duplicate definition" error if we emit the generated code. - if ((method.Name == "op_Equality" || method.Name == "op_Inequality") - && method.Parameters.Count == 2 - && method.Parameters.All(p => p.Type.GetDefinition() == typeDef)) - { - // Don't emit comparison operators into C# record definition - // Note: user can declare additional operator== as long as they have - // different parameter types. - continue; - } - if (method.Name == "Equals" && method.Parameters.Count == 1 && method.Parameters[0].Type.IsKnownType(KnownTypeCode.Object)) - { - // override bool Equals(object? obj) - // Note: the "virtual bool Equals(R? other)" overload can be customized - continue; - } - if (method.Name == "$" && method.Parameters.Count == 0) - { - // Method name cannot be expressed in C# - continue; - } + continue; } if (!method.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, method.MetadataToken, settings)) { diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs new file mode 100644 index 000000000..d474ac0c3 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -0,0 +1,145 @@ + +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection.Metadata; +using System.Threading; + +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.CSharp +{ + class RecordDecompiler + { + readonly IDecompilerTypeSystem typeSystem; + readonly ITypeDefinition recordTypeDef; + readonly CancellationToken cancellationToken; + + public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) + { + this.typeSystem = dts; + this.recordTypeDef = recordTypeDef; + this.cancellationToken = cancellationToken; + } + + bool IsRecordType(IType type) + { + return type.GetDefinition() == recordTypeDef + && type.TypeArguments.SequenceEqual(recordTypeDef.TypeParameters); + } + + /// + /// Gets whether the member of the record type will be automatically generated by the compiler. + /// + public bool MethodIsGenerated(IMethod method) + { + switch (method.Name) + { + // Some members in records are always compiler-generated and lead to a + // "duplicate definition" error if we emit the generated code. + case "op_Equality": + case "op_Inequality": + { + // Don't emit comparison operators into C# record definition + // Note: user can declare additional operator== as long as they have + // different parameter types. + return method.Parameters.Count == 2 + && method.Parameters.All(p => IsRecordType(p.Type)); + } + case "Equals" when method.Parameters.Count == 1: + { + IType paramType = method.Parameters[0].Type; + if (paramType.IsKnownType(KnownTypeCode.Object)) + { + // override bool Equals(object? obj): always generated + return true; + } + else if (IsRecordType(paramType)) + { + // virtual bool Equals(R? other): generated unless user-declared + return false; + } + else + { + return false; + } + } + case "$" when method.Parameters.Count == 0: + // Always generated; Method name cannot be expressed in C# + return true; + default: + return false; + } + } + + internal bool PropertyIsGenerated(IProperty property) + { + switch (property.Name) + { + case "EqualityContract": + return IsGeneratedEqualityContract(property); + default: + return false; + } + } + + private bool IsGeneratedEqualityContract(IProperty property) + { + // Generated member: + // protected virtual Type EqualityContract { + // [CompilerGenerated] get => typeof(R); + // } + Debug.Assert(property.Name == "EqualityContract"); + if (property.Accessibility != Accessibility.Protected) + return false; + if (!(property.IsVirtual || property.IsOverride)) + return false; + var getter = property.Getter; + if (!(getter != null && !property.CanSet)) + return false; + if (property.GetAttributes().Any()) + return false; + if (getter.GetReturnTypeAttributes().Any()) + return false; + var attrs = getter.GetAttributes().ToList(); + if (attrs.Count != 1) + return false; + if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated)) + return false; + var body = DecompileBody(getter); + if (!(body?.SingleInstruction() is Leave leave)) + return false; + // leave IL_0000 (call GetTypeFromHandle(ldtypetoken R)) + if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty)) + return false; + return IsRecordType(ty); + } + + BlockContainer DecompileBody(IMethod method) + { + if (method == null || method.MetadataToken.IsNil) + return null; + var metadata = typeSystem.MainModule.metadata; + + var methodDefHandle = (MethodDefinitionHandle)method.MetadataToken; + var methodDef = metadata.GetMethodDefinition(methodDefHandle); + if (!methodDef.HasBody()) + return null; + + var genericContext = new GenericContext( + classTypeParameters: recordTypeDef.TypeParameters, + methodTypeParameters: null); + var body = typeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress); + var ilReader = new ILReader(typeSystem.MainModule); + var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken); + var settings = new DecompilerSettings(LanguageVersion.CSharp2); + il.RunTransforms(CSharpDecompiler.EarlyILTransforms(), + new ILTransformContext(il, typeSystem, debugInfo: null, settings) { + CancellationToken = cancellationToken + }); + return (BlockContainer)il.Body; + } + } +} diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 6d00a9407..72ada7b39 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -67,6 +67,7 @@ + From 0babcc5fe404030bef934b971cf0ca76832f2f40 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 12 Nov 2020 23:58:21 +0100 Subject: [PATCH 05/16] Records: detect when ToString() is compiler-generated --- .../CSharp/RecordDecompiler.cs | 98 ++++++++++++++++++- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index d474ac0c3..b0c90b5a6 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -69,6 +69,8 @@ namespace ICSharpCode.Decompiler.CSharp case "$" when method.Parameters.Count == 0: // Always generated; Method name cannot be expressed in C# return true; + case "ToString" when method.Parameters.Count == 0: + return IsGeneratedToString(method); default: return false; } @@ -96,6 +98,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!(property.IsVirtual || property.IsOverride)) return false; + if (property.IsSealed) + return false; var getter = property.Getter; if (!(getter != null && !property.CanSet)) return false; @@ -109,7 +113,9 @@ namespace ICSharpCode.Decompiler.CSharp if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated)) return false; var body = DecompileBody(getter); - if (!(body?.SingleInstruction() is Leave leave)) + if (body == null || body.Instructions.Count != 1) + return false; + if (!(body.Instructions.Single() is Leave leave)) return false; // leave IL_0000 (call GetTypeFromHandle(ldtypetoken R)) if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty)) @@ -117,7 +123,74 @@ namespace ICSharpCode.Decompiler.CSharp return IsRecordType(ty); } - BlockContainer DecompileBody(IMethod method) + private bool IsGeneratedToString(IMethod method) + { + Debug.Assert(method.Name == "ToString"); + if (!method.IsOverride) + return false; + if (method.IsSealed) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + // stloc stringBuilder(newobj StringBuilder..ctor()) + if (!body.Instructions[0].MatchStLoc(out var stringBuilder, out var stringBuilderInit)) + return false; + if (!(stringBuilderInit is NewObj { Arguments: { Count: 0 }, Method: { DeclaringTypeDefinition: { Name: "StringBuilder", Namespace: "System.Text" } } })) + return false; + // callvirt Append(ldloc stringBuilder, ldstr "R") + if (!MatchAppendCallWithValue(body.Instructions[1], recordTypeDef.Name)) + return false; + // callvirt Append(ldloc stringBuilder, ldstr " { ") + if (!MatchAppendCallWithValue(body.Instructions[2], " { ")) + return false; + // if (callvirt PrintMembers(ldloc this, ldloc stringBuilder)) { trueInst } + if (!body.Instructions[3].MatchIfInstruction(out var condition, out var trueInst)) + return true; + if (!(condition is CallVirt { Method: { Name: "PrintMembers" } } printMembersCall)) + return false; + if (printMembersCall.Arguments.Count != 2) + return false; + if (!printMembersCall.Arguments[0].MatchLdThis()) + return false; + if (!printMembersCall.Arguments[1].MatchLdLoc(stringBuilder)) + return false; + // trueInst: callvirt Append(ldloc stringBuilder, ldstr " ") + if (!MatchAppendCallWithValue(Block.Unwrap(trueInst), " ")) + return false; + // callvirt Append(ldloc stringBuilder, ldstr "}") + if (!MatchAppendCallWithValue(body.Instructions[4], "}")) + return false; + // leave IL_0000 (callvirt ToString(ldloc stringBuilder)) + if (!(body.Instructions[5] is Leave leave)) + return false; + if (!(leave.Value is CallVirt { Method: { Name: "ToString" } } toStringCall)) + return false; + if (toStringCall.Arguments.Count != 1) + return false; + return toStringCall.Arguments[0].MatchLdLoc(stringBuilder); + + bool MatchAppendCall(ILInstruction inst, out string val) + { + val = null; + if (!(inst is CallVirt { Method: { Name: "Append" } } call)) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Arguments[0].MatchLdLoc(stringBuilder)) + return false; + return call.Arguments[1].MatchLdStr(out val); + } + + bool MatchAppendCallWithValue(ILInstruction inst, string val) + { + return MatchAppendCall(inst, out string tmp) && tmp == val; + } + } + + Block DecompileBody(IMethod method) { if (method == null || method.MetadataToken.IsNil) return null; @@ -134,12 +207,27 @@ namespace ICSharpCode.Decompiler.CSharp var body = typeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress); var ilReader = new ILReader(typeSystem.MainModule); var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken); - var settings = new DecompilerSettings(LanguageVersion.CSharp2); - il.RunTransforms(CSharpDecompiler.EarlyILTransforms(), + var settings = new DecompilerSettings(LanguageVersion.CSharp1); + var transforms = CSharpDecompiler.GetILTransforms(); + // Remove the last couple transforms -- we don't need variable names etc. here + int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform); + transforms.RemoveRange(lastBlockTransform + 1, transforms.Count - (lastBlockTransform + 1)); + il.RunTransforms(transforms, new ILTransformContext(il, typeSystem, debugInfo: null, settings) { CancellationToken = cancellationToken }); - return (BlockContainer)il.Body; + if (il.Body is BlockContainer container) + { + return container.EntryPoint; + } + else if (il.Body is Block block) + { + return block; + } + else + { + return null; + } } } } From 500317a9e881d03336329fafb60dc2dfbf8c3e63 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 22 Dec 2020 23:24:09 +0100 Subject: [PATCH 06/16] Records: detect when PrintMembers() is compiler-generated Only for the simple case: record having only (auto-)properties but no fields and no inheritance. --- .../CSharp/RecordDecompiler.cs | 138 +++++++++++++++++- ILSpy/ILSpy.csproj | 2 +- ILSpy/Languages/CSharpLanguage.cs | 2 +- 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index b0c90b5a6..9b0d1c411 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -1,5 +1,23 @@ - +// Copyright (c) 2020 Daniel Grunwald +// +// 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; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection.Metadata; @@ -16,12 +34,29 @@ namespace ICSharpCode.Decompiler.CSharp readonly IDecompilerTypeSystem typeSystem; readonly ITypeDefinition recordTypeDef; readonly CancellationToken cancellationToken; + readonly List orderedMembers; public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) { this.typeSystem = dts; this.recordTypeDef = recordTypeDef; this.cancellationToken = cancellationToken; + this.orderedMembers = DetectMemberOrder(recordTypeDef); + } + + static List DetectMemberOrder(ITypeDefinition recordTypeDef) + { + // For records, the order of members is important: + // Equals/GetHashCode/PrintMembers must agree on an order of fields+properties. + // The IL metadata has the order of fields and the order of properties, but we + // need to detect the correct interleaving. + // We could try to detect this from the PrintMembers body, but let's initially + // restrict ourselves to the common case where the record only uses properties. + if (recordTypeDef.Fields.All(f => f.Name.StartsWith("<", StringComparison.Ordinal) && f.Name.EndsWith("BackingField", StringComparison.Ordinal))) + { + return recordTypeDef.Properties.ToList(); + } + return null; } bool IsRecordType(IType type) @@ -69,6 +104,8 @@ namespace ICSharpCode.Decompiler.CSharp case "$" when method.Parameters.Count == 0: // Always generated; Method name cannot be expressed in C# return true; + case "PrintMembers": + return IsGeneratedPrintMembers(method); case "ToString" when method.Parameters.Count == 0: return IsGeneratedToString(method); default: @@ -122,10 +159,107 @@ namespace ICSharpCode.Decompiler.CSharp return false; return IsRecordType(ty); } + private bool IsGeneratedPrintMembers(IMethod method) + { + Debug.Assert(method.Name == "PrintMembers"); + if (method.Parameters.Count != 1) + return false; + if (!method.IsOverridable) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (orderedMembers == null) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + var variables = body.Ancestors.OfType().Single().Variables; + var builder = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0); + if (builder.Type.ReflectionName != "System.Text.StringBuilder") + return false; + int pos = 0; + bool needsComma = false; + foreach (var member in orderedMembers) + { + if (member.Name == "EqualityContract") + { + continue; // EqualityContract is never printed + } + /* + callvirt Append(ldloc builder, ldstr "A") + callvirt Append(ldloc builder, ldstr " = ") + callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(addressof System.Int32(call get_A(ldloc this)))) + callvirt Append(ldloc builder, ldstr ", ") + callvirt Append(ldloc builder, ldstr "B") + callvirt Append(ldloc builder, ldstr " = ") + callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(ldflda B(ldloc this))) + leave IL_0000 (ldc.i4 1) */ + if (!MatchStringBuilderAppendConstant(out string text)) + return false; + string expectedText = (needsComma ? ", " : "") + member.Name + " = "; + if (text != expectedText) + return false; + if (!MatchStringBuilderAppend(body.Instructions[pos], builder, out var val)) + return false; + if (val is CallInstruction { Method: { Name: "ToString", IsStatic: false } } toStringCall) + { + if (toStringCall.Arguments.Count != 1) + return false; + val = toStringCall.Arguments[0]; + if (val is AddressOf addressOf) + { + val = addressOf.Value; + } + } + if (val is CallInstruction getterCall && member is IProperty property) + { + if (!getterCall.Method.MemberDefinition.Equals(property.Getter.MemberDefinition)) + return false; + if (getterCall.Arguments.Count != 1) + return false; + if (!getterCall.Arguments[0].MatchLdThis()) + return false; + } + else + { + return false; + } + pos++; + needsComma = true; + } + // leave IL_0000 (ldc.i4 1) + return body.Instructions[pos].MatchReturn(out var retVal) + && retVal.MatchLdcI4(orderedMembers.Count > 0 ? 1 : 0); + + + bool MatchStringBuilderAppendConstant(out string text) + { + text = null; + while (MatchStringBuilderAppend(body.Instructions[pos], builder, out var val) && val.MatchLdStr(out string valText)) + { + text += valText; + pos++; + } + return text != null; + } + } + + private bool MatchStringBuilderAppend(ILInstruction inst, ILVariable sb, out ILInstruction val) + { + val = null; + if (!(inst is CallVirt { Method: { Name: "Append", DeclaringType: { Namespace: "System.Text", Name: "StringBuilder" } } } call)) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Arguments[0].MatchLdLoc(sb)) + return false; + val = call.Arguments[1]; + return true; + } private bool IsGeneratedToString(IMethod method) { - Debug.Assert(method.Name == "ToString"); + Debug.Assert(method.Name == "ToString" && method.Parameters.Count == 0); if (!method.IsOverride) return false; if (method.IsSealed) diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 7d24f4ee7..e8cf6e6db 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -465,7 +465,7 @@ PublicResXFileCodeGenerator Resources.Designer.cs - + diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 4d257d0d6..38e2ab6a4 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -110,7 +110,7 @@ namespace ICSharpCode.ILSpy new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_2.ToString(), "C# 7.2 / VS 2017.4"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_3.ToString(), "C# 7.3 / VS 2017.7"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp8_0.ToString(), "C# 8.0 / VS 2019"), - new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 (experimental)"), + new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 / VS 2019.8"), }; } return versions; From 4fefd5f5302126bc8e2f0e2d39673a3b7bcd0251 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 22 Dec 2020 23:38:17 +0100 Subject: [PATCH 07/16] Records: detect when PrintMembers() is compiler-generated in derived records --- .../CSharp/RecordDecompiler.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 9b0d1c411..996091b56 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -35,12 +35,14 @@ namespace ICSharpCode.Decompiler.CSharp readonly ITypeDefinition recordTypeDef; readonly CancellationToken cancellationToken; readonly List orderedMembers; + readonly bool isInheritedRecord; public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) { this.typeSystem = dts; this.recordTypeDef = recordTypeDef; this.cancellationToken = cancellationToken; + this.isInheritedRecord = recordTypeDef.DirectBaseTypes.Any(b => b.Kind == TypeKind.Class && !b.IsKnownType(KnownTypeCode.Object)); this.orderedMembers = DetectMemberOrder(recordTypeDef); } @@ -178,6 +180,29 @@ namespace ICSharpCode.Decompiler.CSharp if (builder.Type.ReflectionName != "System.Text.StringBuilder") return false; int pos = 0; + if (isInheritedRecord) + { + // if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f { + // callvirt Append(ldloc builder, ldstr ", ") + // } + if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst)) + return false; + if (!(condition is CallInstruction { Method: { Name: "PrintMembers" } } call)) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Arguments[0].MatchLdThis()) + return false; + if (!call.Arguments[1].MatchLdLoc(builder)) + return false; + // trueInst = callvirt Append(ldloc builder, ldstr ", ") + trueInst = Block.Unwrap(trueInst); + if (!MatchStringBuilderAppend(trueInst, builder, out var val)) + return false; + if (!(val.MatchLdStr(out string text) && text == ", ")) + return false; + pos++; + } bool needsComma = false; foreach (var member in orderedMembers) { @@ -229,7 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp } // leave IL_0000 (ldc.i4 1) return body.Instructions[pos].MatchReturn(out var retVal) - && retVal.MatchLdcI4(orderedMembers.Count > 0 ? 1 : 0); + && retVal.MatchLdcI4(needsComma ? 1 : 0); bool MatchStringBuilderAppendConstant(out string text) From 648e7f9f87297a5999830b38814144daed6c5993 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 24 Dec 2020 00:06:19 +0100 Subject: [PATCH 08/16] Records: hide generated Equals() method --- .../CSharp/RecordDecompiler.cs | 265 +++++++++++++++++- 1 file changed, 261 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 996091b56..45bd140d6 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -36,6 +36,8 @@ namespace ICSharpCode.Decompiler.CSharp readonly CancellationToken cancellationToken; readonly List orderedMembers; readonly bool isInheritedRecord; + readonly Dictionary backingFieldToAutoProperty = new Dictionary(); + readonly Dictionary autoPropertyToBackingField = new Dictionary(); public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) { @@ -43,10 +45,104 @@ namespace ICSharpCode.Decompiler.CSharp this.recordTypeDef = recordTypeDef; this.cancellationToken = cancellationToken; this.isInheritedRecord = recordTypeDef.DirectBaseTypes.Any(b => b.Kind == TypeKind.Class && !b.IsKnownType(KnownTypeCode.Object)); - this.orderedMembers = DetectMemberOrder(recordTypeDef); + DetectAutomaticProperties(); + this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); } - static List DetectMemberOrder(ITypeDefinition recordTypeDef) + void DetectAutomaticProperties() + { + foreach (var p in recordTypeDef.Properties) + { + cancellationToken.ThrowIfCancellationRequested(); + if (IsAutoProperty(p, out var field)) + { + backingFieldToAutoProperty.Add(field, p); + autoPropertyToBackingField.Add(p, field); + } + } + + bool IsAutoProperty(IProperty p, out IField field) + { + field = null; + if (p.Parameters.Count != 0) + return false; + if (p.Getter != null) + { + if (!IsAutoGetter(p.Getter, out field)) + return false; + } + if (p.Setter != null) + { + if (!IsAutoSetter(p.Setter, out var field2)) + return false; + if (field != null) + { + if (!field.Equals(field2)) + return false; + } + else + { + field = field2; + } + } + if (field == null) + return false; + if (!IsRecordType(field.DeclaringType)) + return false; + return field.Name == $"<{p.Name}>k__BackingField"; + } + + bool IsAutoGetter(IMethod method, out IField field) + { + field = null; + var body = DecompileBody(method); + if (body == null) + return false; + // return this.field; + if (!body.Instructions[0].MatchReturn(out var retVal)) + return false; + if (method.IsStatic) + { + return retVal.MatchLdsFld(out field); + } + else + { + if (!retVal.MatchLdFld(out var target, out field)) + return false; + return target.MatchLdThis(); + } + } + + bool IsAutoSetter(IMethod method, out IField field) + { + field = null; + Debug.Assert(!method.IsStatic); + var body = DecompileBody(method); + if (body == null) + return false; + // this.field = value; + ILInstruction valueInst; + if (method.IsStatic) + { + if (!body.Instructions[0].MatchStsFld(out field, out valueInst)) + return false; + } + else + { + if (!body.Instructions[0].MatchStFld(out var target, out field, out valueInst)) + return false; + if (!target.MatchLdThis()) + return false; + } + if (!valueInst.MatchLdLoc(out var value)) + return false; + if (!(value.Kind == VariableKind.Parameter && value.Index == 0)) + return false; + return body.Instructions[1].MatchReturn(out var retVal) && retVal.MatchNop(); + } + } + + static List DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary backingFieldToAutoProperty) { // For records, the order of members is important: // Equals/GetHashCode/PrintMembers must agree on an order of fields+properties. @@ -54,7 +150,7 @@ namespace ICSharpCode.Decompiler.CSharp // need to detect the correct interleaving. // We could try to detect this from the PrintMembers body, but let's initially // restrict ourselves to the common case where the record only uses properties. - if (recordTypeDef.Fields.All(f => f.Name.StartsWith("<", StringComparison.Ordinal) && f.Name.EndsWith("BackingField", StringComparison.Ordinal))) + if (recordTypeDef.Fields.All(backingFieldToAutoProperty.ContainsKey)) { return recordTypeDef.Properties.ToList(); } @@ -96,7 +192,7 @@ namespace ICSharpCode.Decompiler.CSharp else if (IsRecordType(paramType)) { // virtual bool Equals(R? other): generated unless user-declared - return false; + return IsGeneratedEquals(method); } else { @@ -161,6 +257,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; return IsRecordType(ty); } + private bool IsGeneratedPrintMembers(IMethod method) { Debug.Assert(method.Name == "PrintMembers"); @@ -206,10 +303,19 @@ namespace ICSharpCode.Decompiler.CSharp bool needsComma = false; foreach (var member in orderedMembers) { + if (member.IsStatic) + { + continue; // static fields/properties are not printed + } if (member.Name == "EqualityContract") { continue; // EqualityContract is never printed } + if (member.IsExplicitInterfaceImplementation) + { + continue; // explicit interface impls are not printed + } + cancellationToken.ThrowIfCancellationRequested(); /* callvirt Append(ldloc builder, ldstr "A") callvirt Append(ldloc builder, ldstr " = ") @@ -349,6 +455,157 @@ namespace ICSharpCode.Decompiler.CSharp } } + private bool IsGeneratedEquals(IMethod method) + { + // virtual bool Equals(R? other) { + // return other != null && EqualityContract == other.EqualityContract && EqualityComparer.Default.Equals(A, other.A) && ...; + // } + Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1); + if (method.Parameters.Count != 1) + return false; + if (!method.IsOverridable) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (orderedMembers == null) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + if (!body.Instructions[0].MatchReturn(out var returnValue)) + return false; + var variables = body.Ancestors.OfType().Single().Variables; + var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0); + Debug.Assert(IsRecordType(other.Type)); + var conditions = UnpackLogicAndChain(returnValue); + Debug.Assert(conditions.Count >= 1); + int pos = 0; + if (isInheritedRecord) + { + return false; // TODO: implement this case + } + else + { + // comp.o(ldloc other != ldnull) + if (pos >= conditions.Count) + return false; + if (!conditions[pos].MatchCompNotEqualsNull(out var arg)) + return false; + if (!arg.MatchLdLoc(other)) + return false; + pos++; + // call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other)) + if (pos >= conditions.Count) + return false; + if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall)) + return false; + if (!opEqualityCall.Method.DeclaringType.IsKnownType(KnownTypeCode.Type)) + return false; + if (opEqualityCall.Arguments.Count != 2) + return false; + if (!MatchGetEqualityContract(opEqualityCall.Arguments[0], out var target1)) + return false; + if (!MatchGetEqualityContract(opEqualityCall.Arguments[1], out var target2)) + return false; + if (!target1.MatchLdThis()) + return false; + if (!target2.MatchLdLoc(other)) + return false; + pos++; + } + foreach (var member in orderedMembers) + { + if (member.IsStatic) + continue; + if (member.Name == "EqualityContract") + continue; // already special-cased + IField field = member as IField; + if (field == null) + { + if (!(member is IProperty property)) + return false; + if (!autoPropertyToBackingField.TryGetValue(property, out field)) + continue; // Equals ignores non-automatic properties + } + // EqualityComparer.Default.Equals(A, other.A) + // callvirt Equals(call get_Default(), ldfld k__BackingField(ldloc this), ldfld k__BackingField(ldloc other)) + if (pos >= conditions.Count) + return false; + if (!(conditions[pos] is CallVirt { Method: { Name: "Equals" } } equalsCall)) + return false; + if (equalsCall.Arguments.Count != 3) + return false; + if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], field.Type)) + return false; + if (!MatchLdFld(equalsCall.Arguments[1], field, out var target1)) + return false; + if (!MatchLdFld(equalsCall.Arguments[2], field, out var target2)) + return false; + if (!target1.MatchLdThis()) + return false; + if (!target2.MatchLdLoc(other)) + return false; + pos++; + } + return pos == conditions.Count; + } + + static bool MatchLdFld(ILInstruction inst, IField field, out ILInstruction target) + { + if (inst.MatchLdFld(out target, out IField f)) + { + return f.Equals(field); + } + else + { + return false; + } + } + + static List UnpackLogicAndChain(ILInstruction rootOfChain) + { + var result = new List(); + Visit(rootOfChain); + return result; + + void Visit(ILInstruction inst) + { + if (inst.MatchLogicAnd(out var lhs, out var rhs)) + { + Visit(lhs); + Visit(rhs); + } + else + { + result.Add(inst); + } + } + } + + private static bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target) + { + target = null; + if (!(inst is CallVirt { Method: { Name: "get_EqualityContract" } } call)) + return false; + if (call.Arguments.Count != 1) + return false; + target = call.Arguments[0]; + return true; + } + + private static bool IsEqualityComparerGetDefaultCall(ILInstruction inst, IType type) + { + if (!(inst is Call { Method: { Name: "get_Default", IsStatic: true } } call)) + return false; + if (!(call.Method.DeclaringType is { Name: "EqualityComparer", Namespace: "System.Collections.Generic" })) + return false; + if (call.Method.DeclaringType.TypeArguments.Count != 1) + return false; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType.TypeArguments[0], type)) + return false; + return call.Arguments.Count == 0; + } + Block DecompileBody(IMethod method) { if (method == null || method.MetadataToken.IsNil) From c95f75d3bc903939849fdd3983c3160dd5410c91 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 25 Dec 2020 18:33:34 +0100 Subject: [PATCH 09/16] Records: detect whether GetHashCode is compiler-generated --- .../CSharp/RecordDecompiler.cs | 181 +++++++++++++++--- 1 file changed, 155 insertions(+), 26 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 45bd140d6..dcd2d5530 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -36,6 +36,7 @@ namespace ICSharpCode.Decompiler.CSharp readonly CancellationToken cancellationToken; readonly List orderedMembers; readonly bool isInheritedRecord; + readonly IType baseClass; readonly Dictionary backingFieldToAutoProperty = new Dictionary(); readonly Dictionary autoPropertyToBackingField = new Dictionary(); @@ -44,7 +45,8 @@ namespace ICSharpCode.Decompiler.CSharp this.typeSystem = dts; this.recordTypeDef = recordTypeDef; this.cancellationToken = cancellationToken; - this.isInheritedRecord = recordTypeDef.DirectBaseTypes.Any(b => b.Kind == TypeKind.Class && !b.IsKnownType(KnownTypeCode.Object)); + this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class); + this.isInheritedRecord = !baseClass.IsKnownType(KnownTypeCode.Object); DetectAutomaticProperties(); this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); } @@ -199,6 +201,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; } } + case "GetHashCode": + return IsGeneratedGetHashCode(method); case "$" when method.Parameters.Count == 0: // Always generated; Method name cannot be expressed in C# return true; @@ -306,7 +310,7 @@ namespace ICSharpCode.Decompiler.CSharp if (member.IsStatic) { continue; // static fields/properties are not printed - } + } if (member.Name == "EqualityContract") { continue; // EqualityContract is never printed @@ -495,6 +499,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; pos++; // call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other)) + // Special-cased because Roslyn isn't using EqualityComparer here. if (pos >= conditions.Count) return false; if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall)) @@ -515,17 +520,11 @@ namespace ICSharpCode.Decompiler.CSharp } foreach (var member in orderedMembers) { - if (member.IsStatic) + if (!MemberConsideredForEquality(member)) continue; if (member.Name == "EqualityContract") - continue; // already special-cased - IField field = member as IField; - if (field == null) { - if (!(member is IProperty property)) - return false; - if (!autoPropertyToBackingField.TryGetValue(property, out field)) - continue; // Equals ignores non-automatic properties + continue; // already special-cased } // EqualityComparer.Default.Equals(A, other.A) // callvirt Equals(call get_Default(), ldfld k__BackingField(ldloc this), ldfld k__BackingField(ldloc other)) @@ -535,33 +534,25 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (equalsCall.Arguments.Count != 3) return false; - if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], field.Type)) + if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], member.ReturnType)) return false; - if (!MatchLdFld(equalsCall.Arguments[1], field, out var target1)) + if (!MatchMemberAccess(equalsCall.Arguments[1], out var target1, out var member1)) return false; - if (!MatchLdFld(equalsCall.Arguments[2], field, out var target2)) + if (!MatchMemberAccess(equalsCall.Arguments[2], out var target2, out var member2)) return false; if (!target1.MatchLdThis()) return false; + if (!member1.Equals(member)) + return false; if (!target2.MatchLdLoc(other)) return false; + if (!member2.Equals(member)) + return false; pos++; } return pos == conditions.Count; } - static bool MatchLdFld(ILInstruction inst, IField field, out ILInstruction target) - { - if (inst.MatchLdFld(out target, out IField f)) - { - return f.Equals(field); - } - else - { - return false; - } - } - static List UnpackLogicAndChain(ILInstruction rootOfChain) { var result = new List(); @@ -581,7 +572,7 @@ namespace ICSharpCode.Decompiler.CSharp } } } - + private static bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target) { target = null; @@ -606,6 +597,144 @@ namespace ICSharpCode.Decompiler.CSharp return call.Arguments.Count == 0; } + bool MemberConsideredForEquality(IMember member) + { + if (member.IsStatic) + return false; + if (member is IProperty property) + { + if (property.Name == "EqualityContract") + return !isInheritedRecord; + return autoPropertyToBackingField.ContainsKey(property); + } + else + { + return member is IField; + } + } + + bool IsGeneratedGetHashCode(IMethod method) + { + /* + return ( + ( + EqualityComparer.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer.Default.GetHashCode(A) + ) * -1521134295 + EqualityComparer.Default.GetHashCode(B) + ) * -1521134295 + EqualityComparer.Default.GetHashCode(C); + */ + Debug.Assert(method.Name == "GetHashCode"); + if (method.Parameters.Count != 0) + return false; + if (!method.IsOverride || method.IsSealed) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (orderedMembers == null) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + if (!body.Instructions[0].MatchReturn(out var returnValue)) + return false; + var hashedMembers = new List(); + bool foundBaseClassHash = false; + if (!Visit(returnValue)) + return false; + if (foundBaseClassHash != isInheritedRecord) + return false; + return orderedMembers.Where(MemberConsideredForEquality).SequenceEqual(hashedMembers); + + bool Visit(ILInstruction inst) + { + if (inst is BinaryNumericInstruction + { + Operator: BinaryNumericOperator.Add, + CheckForOverflow: false, + Left: BinaryNumericInstruction + { + Operator: BinaryNumericOperator.Mul, + CheckForOverflow: false, + Left: var left, + Right: LdcI4 { Value: -1521134295 } + }, + Right: var right + }) + { + if (!Visit(left)) + return false; + return ProcessIndividualHashCode(right); + } + else + { + return ProcessIndividualHashCode(inst); + } + } + + bool ProcessIndividualHashCode(ILInstruction inst) + { + // base.GetHashCode(): call GetHashCode(ldloc this) + if (inst is Call { Method: { Name: "GetHashCode" } } baseHashCodeCall) + { + if (baseHashCodeCall.Arguments.Count != 1) + return false; + if (!baseHashCodeCall.Arguments[0].MatchLdThis()) + return false; + if (foundBaseClassHash || hashedMembers.Count > 0) + return false; // must be first + foundBaseClassHash = true; + return baseHashCodeCall.Method.DeclaringType.Equals(baseClass); + } + // callvirt GetHashCode(call get_Default(), callvirt get_EqualityContract(ldloc this)) + // callvirt GetHashCode(call get_Default(), ldfld k__BackingField(ldloc this))) + if (!(inst is CallVirt { Method: { Name: "GetHashCode" } } getHashCodeCall)) + return false; + if (getHashCodeCall.Arguments.Count != 2) + return false; + // getHashCodeCall.Arguments[0] checked later + if (!MatchMemberAccess(getHashCodeCall.Arguments[1], out var target, out var member)) + return false; + if (!target.MatchLdThis()) + return false; + if (!IsEqualityComparerGetDefaultCall(getHashCodeCall.Arguments[0], member.ReturnType)) + return false; + hashedMembers.Add(member); + return true; + } + } + + bool MatchMemberAccess(ILInstruction inst, out ILInstruction target, out IMember member) + { + target = null; + member = null; + if (inst is CallVirt + { + Method: + { + AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter, + AccessorOwner: IProperty property + } + } call) + { + if (call.Arguments.Count != 1) + return false; + target = call.Arguments[0]; + member = property; + return true; + } + else if (inst.MatchLdFld(out target, out IField field)) + { + if (backingFieldToAutoProperty.TryGetValue(field, out property)) + member = property; + else + member = field; + return true; + } + else + { + return false; + } + } + Block DecompileBody(IMethod method) { if (method == null || method.MetadataToken.IsNil) From 4050d39b2826aaf2a5d4c92e9139315450162b77 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 25 Dec 2020 18:33:55 +0100 Subject: [PATCH 10/16] Hide [NullableContext] on accessors --- .../TypeSystem/Implementation/AttributeListBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs index 6de21c146..5b887dd82 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs @@ -244,7 +244,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation case "NullableAttribute": return (options & TypeSystemOptions.NullabilityAnnotations) != 0; case "NullableContextAttribute": - return (options & TypeSystemOptions.NullabilityAnnotations) != 0 && (target == SymbolKind.TypeDefinition || target == SymbolKind.Method); + return (options & TypeSystemOptions.NullabilityAnnotations) != 0 + && (target == SymbolKind.TypeDefinition || target == SymbolKind.Method || target == SymbolKind.Accessor); default: return false; } From e02c4789f150282d9a53540010ca7ce0a3d3b88c Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 25 Dec 2020 21:16:38 +0100 Subject: [PATCH 11/16] Records: detect copy constructor --- .../CSharp/RecordDecompiler.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index dcd2d5530..b66f1dad6 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -170,6 +170,11 @@ namespace ICSharpCode.Decompiler.CSharp /// public bool MethodIsGenerated(IMethod method) { + if (method.IsConstructor && method.Parameters.Count == 1 + && IsRecordType(method.Parameters[0].Type)) + { + return IsGeneratedCopyConstructor(method); + } switch (method.Name) { // Some members in records are always compiler-generated and lead to a @@ -226,6 +231,68 @@ namespace ICSharpCode.Decompiler.CSharp } } + private bool IsGeneratedCopyConstructor(IMethod method) + { + /* + call BaseClass..ctor(ldloc this, ldloc original) + stfld k__BackingField(ldloc this, ldfld k__BackingField(ldloc original)) + leave IL_0000 (nop) + */ + Debug.Assert(method.IsConstructor && method.Parameters.Count == 1); + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (orderedMembers == null) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + var variables = body.Ancestors.OfType().Single().Variables; + var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0); + Debug.Assert(IsRecordType(other.Type)); + int pos = 0; + // First instruction is the base constructor call + if (!(body.Instructions[pos] is Call { Method: { IsConstructor: true } } baseCtorCall)) + return false; + if (!object.Equals(baseCtorCall.Method.DeclaringType, baseClass)) + return false; + if (baseCtorCall.Arguments.Count != (isInheritedRecord ? 2 : 1)) + return false; + if (!baseCtorCall.Arguments[0].MatchLdThis()) + return false; + if (isInheritedRecord) + { + if (!baseCtorCall.Arguments[1].MatchLdLoc(other)) + return false; + } + pos++; + // Then all the fields are copied over + foreach (var member in orderedMembers) + { + if (!(member is IField field)) + { + if (!autoPropertyToBackingField.TryGetValue((IProperty)member, out field)) + continue; + } + if (pos >= body.Instructions.Count) + return false; + if (!body.Instructions[pos].MatchStFld(out var lhsTarget, out var lhsField, out var valueInst)) + return false; + if (!lhsTarget.MatchLdThis()) + return false; + if (!lhsField.Equals(field)) + return false; + + if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField)) + return false; + if (!rhsTarget.MatchLdThis()) + return false; + if (!rhsField.Equals(field)) + return false; + pos++; + } + return body.Instructions[pos] is Leave; + } + private bool IsGeneratedEqualityContract(IProperty property) { // Generated member: From a960216d5f629b4ae2b8048879a5fa430560ca60 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 28 Dec 2020 20:02:02 +0100 Subject: [PATCH 12/16] Add test case for simple records. --- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../PrettyTestRunner.cs | 6 ++++ .../TestCases/Pretty/Records.cs | 35 +++++++++++++++++++ .../CSharp/RecordDecompiler.cs | 4 ++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index cdcab56dc..02b24a0d1 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -105,6 +105,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 3c674d65a..d0ab94223 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -404,6 +404,12 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); } + [Test] + public void Records([ValueSource(nameof(roslynLatestOnlyOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); + } + [Test] public void NullPropagation([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs new file mode 100644 index 000000000..d3c4e26f0 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -0,0 +1,35 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public record Empty + { + } + + 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; + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index b66f1dad6..a259fb128 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -284,7 +284,7 @@ namespace ICSharpCode.Decompiler.CSharp if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField)) return false; - if (!rhsTarget.MatchLdThis()) + if (!rhsTarget.MatchLdLoc(other)) return false; if (!rhsField.Equals(field)) return false; @@ -824,6 +824,8 @@ namespace ICSharpCode.Decompiler.CSharp // Remove the last couple transforms -- we don't need variable names etc. here int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform); transforms.RemoveRange(lastBlockTransform + 1, transforms.Count - (lastBlockTransform + 1)); + // Use CombineExitsTransform so that "return other != null && ...;" is a single statement even in release builds + transforms.Add(new CombineExitsTransform()); il.RunTransforms(transforms, new ILTransformContext(il, typeSystem, debugInfo: null, settings) { CancellationToken = cancellationToken From f3a65c7672b9d8037799ec9eb46817630df7b2ff Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 29 Dec 2020 17:42:10 +0100 Subject: [PATCH 13/16] Remove `(experimental)` label from C# 9.0 settings. --- ICSharpCode.Decompiler/DecompilerSettings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index b237678b4..99e600a66 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -176,7 +176,7 @@ namespace ICSharpCode.Decompiler /// /// Use C# 9 nint/nuint types. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.NativeIntegers")] public bool NativeIntegers { get { return nativeIntegers; } @@ -194,7 +194,7 @@ namespace ICSharpCode.Decompiler /// /// Use C# 9 init; property accessors. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.InitAccessors")] public bool InitAccessors { get { return initAccessors; } @@ -210,9 +210,9 @@ namespace ICSharpCode.Decompiler bool recordClasses = true; /// - /// Use C# 9 init; property accessors. + /// Use C# 9 record classes. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.RecordClasses")] public bool RecordClasses { get { return recordClasses; } @@ -231,7 +231,7 @@ namespace ICSharpCode.Decompiler /// Use C# 9 delegate* unmanaged types. /// If this option is disabled, function pointers will instead be decompiled with type `IntPtr`. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.FunctionPointers")] public bool FunctionPointers { get { return functionPointers; } @@ -629,7 +629,7 @@ namespace ICSharpCode.Decompiler /// /// Support GetEnumerator extension methods in foreach. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.DecompileForEachWithGetEnumeratorExtension")] public bool ForEachWithGetEnumeratorExtension { get { return forEachWithGetEnumeratorExtension; } From 0bf6d552e09cced5da45aefbb5b4c094967cf12a Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 29 Dec 2020 18:06:06 +0100 Subject: [PATCH 14/16] Records: support for fields. --- .../TestCases/Pretty/Records.cs | 9 +++++++ .../CSharp/CSharpDecompiler.cs | 24 +++++++++++-------- .../CSharp/RecordDecompiler.cs | 21 ++++++++++++---- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index d3c4e26f0..07865dc19 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -4,6 +4,15 @@ { } + public record Fields + { + public int A; + public double B = 1.0; + public object C; + public dynamic D; + public string S = "abc"; + } + public record Properties { public int A { diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 9ac8dc2b4..02b15fdda 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1179,24 +1179,28 @@ namespace ICSharpCode.Decompiler.CSharp typeDecl.Members.Add(nestedType); } } - foreach (var field in typeDef.Fields) + // With C# 9 records, the relative order of fields and properties matters: + IEnumerable fieldsAndProperties = recordDecompiler?.FieldsAndProperties + ?? typeDef.Fields.Concat(typeDef.Properties); + foreach (var fieldOrProperty in fieldsAndProperties) { - if (!field.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, field.MetadataToken, settings)) + if (fieldOrProperty.MetadataToken.IsNil || MemberIsHidden(module.PEFile, fieldOrProperty.MetadataToken, settings)) + { + continue; + } + if (fieldOrProperty is IField field) { if (typeDef.Kind == TypeKind.Enum && !field.IsConst) continue; var memberDecl = DoDecompile(field, decompileRun, decompilationContext.WithCurrentMember(field)); typeDecl.Members.Add(memberDecl); } - } - foreach (var property in typeDef.Properties) - { - if (recordDecompiler?.PropertyIsGenerated(property) == true) - { - continue; - } - if (!property.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, property.MetadataToken, settings)) + else if (fieldOrProperty is IProperty property) { + if (recordDecompiler?.PropertyIsGenerated(property) == true) + { + continue; + } var propDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property)); typeDecl.Members.Add(propDecl); } diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index a259fb128..da7d74b49 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -152,13 +152,17 @@ namespace ICSharpCode.Decompiler.CSharp // need to detect the correct interleaving. // We could try to detect this from the PrintMembers body, but let's initially // restrict ourselves to the common case where the record only uses properties. - if (recordTypeDef.Fields.All(backingFieldToAutoProperty.ContainsKey)) - { - return recordTypeDef.Properties.ToList(); - } - return null; + return recordTypeDef.Properties.Concat( + recordTypeDef.Fields.Where(f => !backingFieldToAutoProperty.ContainsKey(f)) + ).ToList(); } + /// + /// Gets the fields and properties of the record type, interleaved as necessary to + /// maintain Equals/ToString/etc. semantics. + /// + public IEnumerable FieldsAndProperties => orderedMembers; + bool IsRecordType(IType type) { return type.GetDefinition() == recordTypeDef @@ -422,6 +426,13 @@ namespace ICSharpCode.Decompiler.CSharp if (!getterCall.Arguments[0].MatchLdThis()) return false; } + else if (val.MatchLdFld(out var target, out var field) || val.MatchLdFlda(out target, out field)) + { + if (!target.MatchLdThis()) + return false; + if (!field.MemberDefinition.Equals(member)) + return false; + } else { return false; From d9874380cde97604a7975f1afa75c79c41fbef3b Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 29 Dec 2020 18:26:30 +0100 Subject: [PATCH 15/16] Records: support generic records --- .../TestCases/Pretty/Records.cs | 18 ++++++++++++++++++ .../CSharp/RecordDecompiler.cs | 19 ++++++++++++++----- .../CSharp/Syntax/TypeSystemAstBuilder.cs | 2 +- .../TypeSystem/TypeSystemExtensions.cs | 17 +++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index 07865dc19..bba7d911b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -13,6 +13,18 @@ public string S = "abc"; } + public record Pair + { + public A First { + get; + init; + } + public B Second { + get; + init; + } + } + public record Properties { public int A { @@ -42,3 +54,9 @@ } } } +namespace System.Runtime.CompilerServices +{ + internal class IsExternalInit + { + } +} diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index da7d74b49..25f80ed02 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -53,9 +53,11 @@ namespace ICSharpCode.Decompiler.CSharp void DetectAutomaticProperties() { - foreach (var p in recordTypeDef.Properties) + var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); + foreach (var property in recordTypeDef.Properties) { cancellationToken.ThrowIfCancellationRequested(); + var p = (IProperty)property.Specialize(subst); if (IsAutoProperty(p, out var field)) { backingFieldToAutoProperty.Add(field, p); @@ -152,8 +154,9 @@ namespace ICSharpCode.Decompiler.CSharp // need to detect the correct interleaving. // We could try to detect this from the PrintMembers body, but let's initially // restrict ourselves to the common case where the record only uses properties. - return recordTypeDef.Properties.Concat( - recordTypeDef.Fields.Where(f => !backingFieldToAutoProperty.ContainsKey(f)) + var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); + return recordTypeDef.Properties.Select(p => p.Specialize(subst)).Concat( + recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst)).Where(f => !backingFieldToAutoProperty.ContainsKey(f)) ).ToList(); } @@ -417,9 +420,15 @@ namespace ICSharpCode.Decompiler.CSharp val = addressOf.Value; } } + else if (val is Box box) + { + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(box.Type, member.ReturnType)) + return false; + val = box.Argument; + } if (val is CallInstruction getterCall && member is IProperty property) { - if (!getterCall.Method.MemberDefinition.Equals(property.Getter.MemberDefinition)) + if (!getterCall.Method.Equals(property.Getter)) return false; if (getterCall.Arguments.Count != 1) return false; @@ -430,7 +439,7 @@ namespace ICSharpCode.Decompiler.CSharp { if (!target.MatchLdThis()) return false; - if (!field.MemberDefinition.Equals(member)) + if (!field.Equals(member)) return false; } else diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 08f87fa5a..1e04e5402 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1804,7 +1804,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax else if (SupportRecordClasses && typeDefinition.IsRecord && baseType.Name == "IEquatable" && baseType.Namespace == "System" && baseType.TypeArguments.Count == 1 - && baseType.TypeArguments[0].Equals(typeDefinition)) + && baseType.TypeArguments[0].Equals(typeDefinition.AsParameterizedType())) { // omit "IEquatable" in records continue; diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index bf25900b6..993804d95 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -616,5 +616,22 @@ namespace ICSharpCode.Decompiler.TypeSystem } return null; } + + /// + /// When given a generic type definition, returns the self-parameterized type + /// (i.e. the type of "this" within the type definition). + /// When given a non-generic type definition, returns that definition unchanged. + /// + public static IType AsParameterizedType(this ITypeDefinition td) + { + if (td.TypeParameterCount == 0) + { + return td; + } + else + { + return new ParameterizedType(td, td.TypeArguments); + } + } } } From be9871981ae3f18bb5c27c2c608ea5a5c990ce65 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 1 Jan 2021 22:03:05 +0100 Subject: [PATCH 16/16] Records: Detect compiler-generated Equals() in derived records. --- .../TestCases/Pretty/Records.cs | 47 +++++++--- .../CSharp/RecordDecompiler.cs | 87 ++++++++++++++----- .../Implementation/AttributeListBuilder.cs | 15 +++- 3 files changed, 117 insertions(+), 32 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index bba7d911b..4afa52c18 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -16,30 +16,30 @@ public record Pair { public A First { - get; - init; + get; + init; } - public B Second { - get; - init; + public B Second { + get; + init; } } public record Properties { - public int A { - get; + public int A { + get; set; } public int B { get; } public int C => 43; - public object O { - get; + public object O { + get; set; } - public string S { + public string S { get; set; } @@ -53,6 +53,33 @@ B = 42; } } + + 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 record DerivedGeneric : Pair where T : struct + { + public bool Flag; + } + + public abstract string AbstractProp { + get; + } + } } namespace System.Runtime.CompilerServices { diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 25f80ed02..026268ab7 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -198,7 +198,7 @@ namespace ICSharpCode.Decompiler.CSharp case "Equals" when method.Parameters.Count == 1: { IType paramType = method.Parameters[0].Type; - if (paramType.IsKnownType(KnownTypeCode.Object)) + if (paramType.IsKnownType(KnownTypeCode.Object) && method.IsOverride) { // override bool Equals(object? obj): always generated return true; @@ -208,6 +208,11 @@ namespace ICSharpCode.Decompiler.CSharp // virtual bool Equals(R? other): generated unless user-declared return IsGeneratedEquals(method); } + else if (isInheritedRecord && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(paramType, baseClass) && method.IsOverride) + { + // override bool Equals(BaseClass? obj): always generated + return true; + } else { return false; @@ -248,6 +253,8 @@ namespace ICSharpCode.Decompiler.CSharp Debug.Assert(method.IsConstructor && method.Parameters.Count == 1); if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) return false; + if (method.Accessibility != Accessibility.Protected) + return false; if (orderedMembers == null) return false; var body = DecompileBody(method); @@ -345,6 +352,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) return false; + if (method.Accessibility != Accessibility.Protected) + return false; if (orderedMembers == null) return false; var body = DecompileBody(method); @@ -357,18 +366,18 @@ namespace ICSharpCode.Decompiler.CSharp int pos = 0; if (isInheritedRecord) { + // Special case: inherited record adding no new members + if (body.Instructions[pos].MatchReturn(out var returnValue) + && IsBaseCall(returnValue) && !orderedMembers.Any(IsPrintedMember)) + { + return true; + } // if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f { // callvirt Append(ldloc builder, ldstr ", ") // } if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst)) return false; - if (!(condition is CallInstruction { Method: { Name: "PrintMembers" } } call)) - return false; - if (call.Arguments.Count != 2) - return false; - if (!call.Arguments[0].MatchLdThis()) - return false; - if (!call.Arguments[1].MatchLdLoc(builder)) + if (!IsBaseCall(condition)) return false; // trueInst = callvirt Append(ldloc builder, ldstr ", ") trueInst = Block.Unwrap(trueInst); @@ -377,22 +386,25 @@ namespace ICSharpCode.Decompiler.CSharp if (!(val.MatchLdStr(out string text) && text == ", ")) return false; pos++; + + bool IsBaseCall(ILInstruction inst) + { + if (!(inst is CallInstruction { Method: { Name: "PrintMembers" } } call)) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Arguments[0].MatchLdThis()) + return false; + if (!call.Arguments[1].MatchLdLoc(builder)) + return false; + return true; + } } bool needsComma = false; foreach (var member in orderedMembers) { - if (member.IsStatic) - { - continue; // static fields/properties are not printed - } - if (member.Name == "EqualityContract") - { - continue; // EqualityContract is never printed - } - if (member.IsExplicitInterfaceImplementation) - { - continue; // explicit interface impls are not printed - } + if (!IsPrintedMember(member)) + continue; cancellationToken.ThrowIfCancellationRequested(); /* callvirt Append(ldloc builder, ldstr "A") @@ -453,6 +465,26 @@ namespace ICSharpCode.Decompiler.CSharp return body.Instructions[pos].MatchReturn(out var retVal) && retVal.MatchLdcI4(needsComma ? 1 : 0); + bool IsPrintedMember(IMember member) + { + if (member.IsStatic) + { + return false; // static fields/properties are not printed + } + if (member.Name == "EqualityContract") + { + return false; // EqualityContract is never printed + } + if (member.IsExplicitInterfaceImplementation) + { + return false; // explicit interface impls are not printed + } + if (member.IsOverride) + { + return false; // override is not printed (again), the virtual base property was already printed + } + return true; + } bool MatchStringBuilderAppendConstant(out string text) { @@ -573,7 +605,20 @@ namespace ICSharpCode.Decompiler.CSharp int pos = 0; if (isInheritedRecord) { - return false; // TODO: implement this case + // call BaseClass::Equals(ldloc this, ldloc other) + if (pos >= conditions.Count) + return false; + if (!(conditions[pos] is Call { Method: { Name: "Equals" } } call)) + return false; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType, baseClass)) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Arguments[0].MatchLdThis()) + return false; + if (!call.Arguments[1].MatchLdLoc(other)) + return false; + pos++; } else { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs index 5b887dd82..ca25a93c1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs @@ -245,7 +245,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return (options & TypeSystemOptions.NullabilityAnnotations) != 0; case "NullableContextAttribute": return (options & TypeSystemOptions.NullabilityAnnotations) != 0 - && (target == SymbolKind.TypeDefinition || target == SymbolKind.Method || target == SymbolKind.Accessor); + && (target == SymbolKind.TypeDefinition || IsMethodLike(target)); default: return false; } @@ -255,6 +255,19 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return false; } } + + static bool IsMethodLike(SymbolKind kind) + { + return kind switch + { + SymbolKind.Method => true, + SymbolKind.Operator => true, + SymbolKind.Constructor => true, + SymbolKind.Destructor => true, + SymbolKind.Accessor => true, + _ => false + }; + } #endregion #region Security Attributes