diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index 6487fe3c7..82c38d720 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -1,7 +1,16 @@ -namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { public record Base(string A); + public record CopyCtor(string A) + { + protected CopyCtor(CopyCtor _) + { + } + } + public record Derived(int B) : Base(B.ToString()); public record Empty; @@ -15,6 +24,12 @@ public string S = "abc"; } + public record Interface(int B) : IRecord; + + public interface IRecord + { + } + public record Pair { public A First { get; init; } @@ -24,13 +39,17 @@ public record PairWithPrimaryCtor(A First, B Second); public record PrimaryCtor(int A, string B); + public record PrimaryCtorWithAttribute([RecordTest("param")] [property: RecordTest("property")][field: RecordTest("field")] int a); public record PrimaryCtorWithField(int A, string B) { - public double C; + public double C = 1.0; + public string D = A + B; } + public record PrimaryCtorWithInParameter(in int A, in string B); public record PrimaryCtorWithProperty(int A, string B) { - public double C { get; init; } + public double C { get; init; } = 1.0; + public string D { get; } = A + B; } public record Properties @@ -48,6 +67,18 @@ } } + [AttributeUsage(AttributeTargets.All)] + public class RecordTestAttribute : Attribute + { + public RecordTestAttribute(string name) + { + } + } + + public sealed record Sealed(string A); + + public sealed record SealedDerived(int B) : Base(B.ToString()); + public class WithExpressionTests { public Fields Test(Fields input) diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 42144c5e6..fc7526041 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1227,7 +1227,31 @@ namespace ICSharpCode.Decompiler.CSharp if (recordDecompiler?.PrimaryConstructor != null) { foreach (var p in recordDecompiler.PrimaryConstructor.Parameters) - typeDecl.PrimaryConstructorParameters.Add(typeSystemAstBuilder.ConvertParameter(p)); + { + ParameterDeclaration pd = typeSystemAstBuilder.ConvertParameter(p); + (IProperty prop, IField field) = recordDecompiler.GetPropertyInfoByPrimaryConstructorParameter(p); + Syntax.Attribute[] attributes = prop.GetAttributes().Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); + if (attributes.Length > 0) + { + var section = new AttributeSection { + AttributeTarget = "property" + }; + section.Attributes.AddRange(attributes); + pd.Attributes.Add(section); + } + attributes = field.GetAttributes() + .Where(a => !PatternStatementTransform.attributeTypesToRemoveFromAutoProperties.Contains(a.AttributeType.FullName)) + .Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray(); + if (attributes.Length > 0) + { + var section = new AttributeSection { + AttributeTarget = "field" + }; + section.Attributes.AddRange(attributes); + pd.Attributes.Add(section); + } + typeDecl.PrimaryConstructorParameters.Add(pd); + } } foreach (var type in typeDef.NestedTypes) diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index aafccba14..82494e34c 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -37,6 +37,8 @@ namespace ICSharpCode.Decompiler.CSharp readonly CancellationToken cancellationToken; readonly List orderedMembers; readonly bool isInheritedRecord; + readonly bool isStruct; + readonly bool isSealed; readonly IMethod primaryCtor; readonly IType baseClass; readonly Dictionary backingFieldToAutoProperty = new Dictionary(); @@ -51,7 +53,9 @@ namespace ICSharpCode.Decompiler.CSharp this.settings = settings; this.cancellationToken = cancellationToken; this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class); - this.isInheritedRecord = !baseClass.IsKnownType(KnownTypeCode.Object); + this.isStruct = baseClass.IsKnownType(KnownTypeCode.ValueType); + this.isInheritedRecord = !isStruct && !baseClass.IsKnownType(KnownTypeCode.Object); + this.isSealed = recordTypeDef.IsSealed; DetectAutomaticProperties(); this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); this.primaryCtor = DetectPrimaryConstructor(); @@ -164,13 +168,15 @@ namespace ICSharpCode.Decompiler.CSharp if (method.IsStatic || !method.IsConstructor) continue; var m = method.Specialize(subst); - if (IsPrimaryConstructor(m)) + if (IsPrimaryConstructor(m, method)) return method; + primaryCtorParameterToAutoProperty.Clear(); + autoPropertyToPrimaryCtorParameter.Clear(); } return null; - bool IsPrimaryConstructor(IMethod method) + bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod) { Debug.Assert(method.IsConstructor); var body = DecompileBody(method); @@ -180,28 +186,37 @@ namespace ICSharpCode.Decompiler.CSharp if (method.Parameters.Count == 0) return false; - if (body.Instructions.Count != method.Parameters.Count + 2) + var addonInst = isStruct ? 1 : 2; + if (body.Instructions.Count < method.Parameters.Count + addonInst) return false; - for (int i = 0; i < body.Instructions.Count - 2; i++) + for (int i = 0; i < method.Parameters.Count; i++) { if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst)) return false; if (!target.MatchLdThis()) return false; + if (method.Parameters[i].IsIn) + { + if (!valueInst.MatchLdObj(out valueInst, out _)) + return false; + } if (!valueInst.MatchLdLoc(out var value)) return false; if (!(value.Kind == VariableKind.Parameter && value.Index == i)) return false; if (!backingFieldToAutoProperty.TryGetValue(field, out var property)) return false; - primaryCtorParameterToAutoProperty.Add(method.Parameters[i], property); - autoPropertyToPrimaryCtorParameter.Add(property, method.Parameters[i]); + primaryCtorParameterToAutoProperty.Add(unspecializedMethod.Parameters[i], property); + autoPropertyToPrimaryCtorParameter.Add(property, unspecializedMethod.Parameters[i]); } - var baseCtorCall = body.Instructions.SecondToLastOrDefault() as CallInstruction; - if (baseCtorCall == null) - return false; + if (!isStruct) + { + var baseCtorCall = body.Instructions.SecondToLastOrDefault() as CallInstruction; + if (baseCtorCall == null) + return false; + } var returnInst = body.Instructions.LastOrDefault(); return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop(); @@ -233,6 +248,8 @@ namespace ICSharpCode.Decompiler.CSharp /// public IMethod PrimaryConstructor => primaryCtor; + public bool IsInheritedRecord => isInheritedRecord; + bool IsRecordType(IType type) { return type.GetDefinition() == recordTypeDef @@ -244,13 +261,9 @@ namespace ICSharpCode.Decompiler.CSharp /// public bool MethodIsGenerated(IMethod method) { - if (method.IsConstructor) + if (IsCopyConstructor(method)) { - if (method.Parameters.Count == 1 - && IsRecordType(method.Parameters[0].Type)) - { - return IsGeneratedCopyConstructor(method); - } + return IsGeneratedCopyConstructor(method); } switch (method.Name) @@ -309,7 +322,7 @@ namespace ICSharpCode.Decompiler.CSharp { switch (property.Name) { - case "EqualityContract": + case "EqualityContract" when !isStruct: return IsGeneratedEqualityContract(property); default: return IsPropertyDeclaredByPrimaryConstructor(property); @@ -323,6 +336,24 @@ namespace ICSharpCode.Decompiler.CSharp && autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst)); } + internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter) + { + var prop = primaryCtorParameterToAutoProperty[parameter]; + return (prop, autoPropertyToBackingField[prop]); + } + + public bool IsCopyConstructor(IMethod method) + { + if (method == null) + return false; + + Debug.Assert(method.DeclaringTypeDefinition == recordTypeDef); + + return method.IsConstructor + && method.Parameters.Count == 1 + && IsRecordType(method.Parameters[0].Type); + } + private bool IsGeneratedCopyConstructor(IMethod method) { /* @@ -333,7 +364,7 @@ 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) + if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private)) return false; if (orderedMembers == null) return false; @@ -393,10 +424,10 @@ namespace ICSharpCode.Decompiler.CSharp // protected virtual Type EqualityContract { // [CompilerGenerated] get => typeof(R); // } - Debug.Assert(property.Name == "EqualityContract"); - if (property.Accessibility != Accessibility.Protected) + Debug.Assert(!isStruct && property.Name == "EqualityContract"); + if (property.Accessibility != Accessibility.Protected && (!isSealed || property.Accessibility != Accessibility.Private)) return false; - if (!(property.IsVirtual || property.IsOverride)) + if (!(isSealed || property.IsVirtual || property.IsOverride)) return false; if (property.IsSealed) return false; @@ -428,11 +459,11 @@ namespace ICSharpCode.Decompiler.CSharp Debug.Assert(method.Name == "PrintMembers"); if (method.Parameters.Count != 1) return false; - if (!method.IsOverridable) + if (!isSealed && !method.IsOverridable) return false; if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) return false; - if (method.Accessibility != Accessibility.Protected) + if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private)) return false; if (orderedMembers == null) return false; @@ -444,6 +475,15 @@ namespace ICSharpCode.Decompiler.CSharp if (builder.Type.ReflectionName != "System.Text.StringBuilder") return false; int pos = 0; + //Roslyn 4.0.0-3.final start to insert an call to RuntimeHelpers.EnsureSufficientExecutionStack() + if (!isStruct && !isInheritedRecord && body.Instructions[pos] is Call + { + Arguments: { Count: 0 }, + Method: { Name: "EnsureSufficientExecutionStack", DeclaringType: { Namespace: "System.Runtime.CompilerServices", Name: "RuntimeHelpers" } } + }) + { + pos++; + } if (isInheritedRecord) { // Special case: inherited record adding no new members @@ -551,7 +591,7 @@ namespace ICSharpCode.Decompiler.CSharp { return false; // static fields/properties are not printed } - if (member.Name == "EqualityContract") + if (!isStruct && member.Name == "EqualityContract") { return false; // EqualityContract is never printed } @@ -617,7 +657,8 @@ namespace ICSharpCode.Decompiler.CSharp // 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)) + if (!((condition is CallInstruction { Method: { Name: "PrintMembers" } } printMembersCall) && + (condition is CallVirt || (isSealed && condition is Call)))) return false; if (printMembersCall.Arguments.Count != 2) return false; @@ -640,21 +681,20 @@ namespace ICSharpCode.Decompiler.CSharp return false; return toStringCall.Arguments[0].MatchLdLoc(stringBuilder); - bool MatchAppendCall(ILInstruction inst, out string val) + bool MatchAppendCallWithValue(ILInstruction inst, 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; + //Roslyn 4.0.0-3.final start to use char for 1 length string + if (call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Char)) + { + return val != null && val.Length == 1 && call.Arguments[1].MatchLdcI4(val[0]); + } + return call.Arguments[1].MatchLdStr(out string val1) && val1 == val; } } @@ -670,7 +710,7 @@ namespace ICSharpCode.Decompiler.CSharp Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1); if (method.Parameters.Count != 1) return false; - if (!method.IsOverridable) + if (!isSealed && !method.IsOverridable) return false; if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) return false; @@ -698,58 +738,61 @@ namespace ICSharpCode.Decompiler.CSharp var conditions = UnpackLogicAndChain(returnValue); Debug.Assert(conditions.Count >= 1); int pos = 0; - if (isInheritedRecord) - { - // 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 + if (!isStruct) { - // 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)) - // 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)) - 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++; + if (isInheritedRecord) + { + // 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 + { + // 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)) + // 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)) + 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 (!MemberConsideredForEquality(member)) continue; - if (member.Name == "EqualityContract") + if (!isStruct && member.Name == "EqualityContract") { continue; // already special-cased } @@ -771,7 +814,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!member1.Equals(member)) return false; - if (!target2.MatchLdLoc(other)) + if (!(isStruct ? target2.MatchLdLoca(other) : target2.MatchLdLoc(other))) return false; if (!member2.Equals(member)) return false; @@ -800,10 +843,12 @@ namespace ICSharpCode.Decompiler.CSharp } } - private static bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target) + private bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target) { target = null; - if (!(inst is CallVirt { Method: { Name: "get_EqualityContract" } } call)) + if (!(inst is CallInstruction { Method: { Name: "get_EqualityContract" } } call)) + return false; + if (!(inst is CallVirt || (isSealed && inst is Call))) return false; if (call.Arguments.Count != 1) return false; @@ -830,7 +875,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (member is IProperty property) { - if (property.Name == "EqualityContract") + if (!isStruct && property.Name == "EqualityContract") return !isInheritedRecord; return autoPropertyToBackingField.ContainsKey(property); } @@ -944,7 +989,10 @@ namespace ICSharpCode.Decompiler.CSharp if (!deconstruct.IsOut) return false; - if (!ctor.Type.Equals(((ByReferenceType)deconstruct.Type).ElementType)) + IType ctorType = ctor.Type; + if (ctor.IsIn) + ctorType = ((ByReferenceType)ctorType).ElementType; + if (!ctorType.Equals(((ByReferenceType)deconstruct.Type).ElementType)) return false; if (ctor.Name != deconstruct.Name) @@ -985,14 +1033,14 @@ namespace ICSharpCode.Decompiler.CSharp { target = null; member = null; - if (inst is CallVirt + if (inst is CallInstruction { Method: { AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter, AccessorOwner: IProperty property } - } call) + } call && (call is CallVirt || (isSealed && call is Call))) { if (call.Arguments.Count != 1) return false; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index e61bbe41c..aaa032971 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -936,7 +936,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms "System.Runtime.CompilerServices.MethodImplAttribute" }; - static readonly string[] attributeTypesToRemoveFromAutoProperties = new[] { + internal static readonly string[] attributeTypesToRemoveFromAutoProperties = new[] { "System.Runtime.CompilerServices.CompilerGeneratedAttribute", "System.Diagnostics.DebuggerBrowsableAttribute" }; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index c285f7fa5..d694ba5eb 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -118,7 +118,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms && currentCtor.Equals(record.PrimaryConstructor) && ci.ConstructorInitializerType == ConstructorInitializerType.Base) { - if (constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl) + if (record.IsInheritedRecord && + constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl) { var baseType = typeDecl.BaseTypes.First(); var newBaseType = new InvocationAstType(); @@ -176,6 +177,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (!context.DecompileRun.RecordDecompilers.TryGetValue(ctorMethodDef.DeclaringTypeDefinition, out var record)) record = null; + //Filter out copy constructor of records + if (record != null) + instanceCtorsNotChainingWithThis = instanceCtorsNotChainingWithThis.Where(ctor => !record.IsCopyConstructor(ctor.GetSymbol() as IMethod)).ToArray(); + // Recognize field or property initializers: // Translate first statement in all ctors (if all ctors have the same statement) into an initializer. bool allSame; @@ -201,9 +206,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (initializer.Annotation()?.Variable.Kind == IL.VariableKind.Parameter) { // remove record ctor parameter assignments - if (IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) - initializer.Remove(); - else + if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) break; } else