From 90ce77f40039f6dff4dbd9e342ce3a203a481e09 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 12 Nov 2020 23:27:44 +0100 Subject: [PATCH] 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 @@ +