diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs index 77819d9f2..9ba4da06a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs @@ -53,15 +53,18 @@ public static class Program [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int pc = pc; + public int pc; [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int current = current; + public int current; public getSeq_00405(int pc, int current) { + this.pc = pc; + this.current = current; + base._002Ector(); } public override int GenerateNext(ref IEnumerable next) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs index 7bdefa44b..da54a4b4c 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs @@ -54,15 +54,18 @@ public static class Program [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int pc = pc; + public int pc; [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] [CompilerGenerated] - public int current = current; + public int current; public getSeq_00405(int pc, int current) { + this.pc = pc; + this.current = current; + base._002Ector(); } public override int GenerateNext(ref IEnumerable next) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index f6f1d635b..6487fe3c7 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -1,8 +1,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { - public record Empty - { - } + public record Base(string A); + + public record Derived(int B) : Base(B.ToString()); + + public record Empty; public record Fields { @@ -19,6 +21,18 @@ public B Second { get; init; } } + public record PairWithPrimaryCtor(A First, B Second); + + public record PrimaryCtor(int A, string B); + public record PrimaryCtorWithField(int A, string B) + { + public double C; + } + public record PrimaryCtorWithProperty(int A, string B) + { + public double C { get; init; } + } + public record Properties { public int A { get; set; } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 02b15fdda..e491caad1 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1169,7 +1169,16 @@ namespace ICSharpCode.Decompiler.CSharp return entityDecl; } bool isRecord = settings.RecordClasses && typeDef.IsRecord; - RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, CancellationToken) : null; + RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null; + if (recordDecompiler != null) + decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler); + + if (recordDecompiler?.PrimaryConstructor != null) + { + foreach (var p in recordDecompiler.PrimaryConstructor.Parameters) + typeDecl.PrimaryConstructorParameters.Add(typeSystemAstBuilder.ConvertParameter(p)); + } + foreach (var type in typeDef.NestedTypes) { if (!type.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, type.MetadataToken, settings)) diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 83cb37d11..4097d9f88 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1557,6 +1557,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor } WriteIdentifier(typeDeclaration.NameToken); WriteTypeParameters(typeDeclaration.TypeParameters); + if (typeDeclaration.PrimaryConstructorParameters.Count > 0) + { + Space(policy.SpaceBeforeMethodDeclarationParentheses); + WriteCommaSeparatedListInParenthesis(typeDeclaration.PrimaryConstructorParameters, policy.SpaceWithinMethodDeclarationParentheses); + } if (typeDeclaration.BaseTypes.Any()) { Space(); @@ -1568,46 +1573,53 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { constraint.AcceptVisitor(this); } - OpenBrace(braceStyle); - if (typeDeclaration.ClassType == ClassType.Enum) + if (typeDeclaration.ClassType == ClassType.RecordClass && typeDeclaration.Members.Count == 0) { - bool first = true; - AstNode last = null; - foreach (var member in typeDeclaration.Members) - { - if (first) - { - first = false; - } - else - { - Comma(member, noSpaceAfterComma: true); - NewLine(); - } - last = member; - member.AcceptVisitor(this); - } - if (last != null) - OptionalComma(last.NextSibling); - NewLine(); + Semicolon(); } else { - bool first = true; - foreach (var member in typeDeclaration.Members) + OpenBrace(braceStyle); + if (typeDeclaration.ClassType == ClassType.Enum) { - if (!first) + bool first = true; + AstNode last = null; + foreach (var member in typeDeclaration.Members) { - for (int i = 0; i < policy.MinimumBlankLinesBetweenMembers; i++) + if (first) + { + first = false; + } + else + { + Comma(member, noSpaceAfterComma: true); NewLine(); + } + last = member; + member.AcceptVisitor(this); + } + if (last != null) + OptionalComma(last.NextSibling); + NewLine(); + } + else + { + bool first = true; + foreach (var member in typeDeclaration.Members) + { + if (!first) + { + for (int i = 0; i < policy.MinimumBlankLinesBetweenMembers; i++) + NewLine(); + } + first = false; + member.AcceptVisitor(this); } - first = false; - member.AcceptVisitor(this); } + CloseBrace(braceStyle); + OptionalSemicolon(typeDeclaration.LastChild); + NewLine(); } - CloseBrace(braceStyle); - OptionalSemicolon(typeDeclaration.LastChild); - NewLine(); EndNode(typeDeclaration); } @@ -2705,6 +2717,16 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor EndNode(functionPointerType); } + public virtual void VisitInvocationType(InvocationAstType invocationType) + { + StartNode(invocationType); + invocationType.BaseType.AcceptVisitor(this); + WriteToken(Roles.LPar); + WriteCommaSeparatedList(invocationType.Arguments); + WriteToken(Roles.RPar); + EndNode(invocationType); + } + public virtual void VisitComposedType(ComposedType composedType) { StartNode(composedType); diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 026268ab7..037de43a3 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -33,22 +33,28 @@ namespace ICSharpCode.Decompiler.CSharp { readonly IDecompilerTypeSystem typeSystem; readonly ITypeDefinition recordTypeDef; + readonly DecompilerSettings settings; readonly CancellationToken cancellationToken; readonly List orderedMembers; readonly bool isInheritedRecord; + readonly IMethod primaryCtor; readonly IType baseClass; readonly Dictionary backingFieldToAutoProperty = new Dictionary(); readonly Dictionary autoPropertyToBackingField = new Dictionary(); + readonly Dictionary primaryCtorParameterToAutoProperty = new Dictionary(); + readonly Dictionary autoPropertyToPrimaryCtorParameter = new Dictionary(); - public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) + public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken) { this.typeSystem = dts; this.recordTypeDef = recordTypeDef; + this.settings = settings; this.cancellationToken = cancellationToken; this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class); this.isInheritedRecord = !baseClass.IsKnownType(KnownTypeCode.Object); DetectAutomaticProperties(); this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); + this.primaryCtor = DetectPrimaryConstructor(); } void DetectAutomaticProperties() @@ -146,6 +152,62 @@ namespace ICSharpCode.Decompiler.CSharp } } + IMethod DetectPrimaryConstructor() + { + if (!settings.UsePrimaryConstructorSyntax) + return null; + + var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); + foreach (var method in recordTypeDef.Methods) + { + cancellationToken.ThrowIfCancellationRequested(); + if (method.IsStatic || !method.IsConstructor) + continue; + var m = method.Specialize(subst); + if (IsPrimaryConstructor(m)) + return method; + } + + return null; + + bool IsPrimaryConstructor(IMethod method) + { + Debug.Assert(method.IsConstructor); + var body = DecompileBody(method); + if (body == null) + return false; + + if (method.Parameters.Count == 0) + return false; + + if (body.Instructions.Count != method.Parameters.Count + 2) + return false; + + for (int i = 0; i < body.Instructions.Count - 2; i++) + { + if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst)) + return false; + if (!target.MatchLdThis()) + 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]); + } + + 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(); + } + } + static List DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary backingFieldToAutoProperty) { // For records, the order of members is important: @@ -166,6 +228,11 @@ namespace ICSharpCode.Decompiler.CSharp /// public IEnumerable FieldsAndProperties => orderedMembers; + /// + /// Gets the detected primary constructor. Returns null, if there was no primary constructor detected. + /// + public IMethod PrimaryConstructor => primaryCtor; + bool IsRecordType(IType type) { return type.GetDefinition() == recordTypeDef @@ -177,11 +244,15 @@ namespace ICSharpCode.Decompiler.CSharp /// public bool MethodIsGenerated(IMethod method) { - if (method.IsConstructor && method.Parameters.Count == 1 - && IsRecordType(method.Parameters[0].Type)) + if (method.IsConstructor) { - return IsGeneratedCopyConstructor(method); + if (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 @@ -227,6 +298,8 @@ namespace ICSharpCode.Decompiler.CSharp return IsGeneratedPrintMembers(method); case "ToString" when method.Parameters.Count == 0: return IsGeneratedToString(method); + case "Deconstruct" when primaryCtor != null && method.Parameters.Count == primaryCtor.Parameters.Count: + return IsGeneratedDeconstruct(method); default: return false; } @@ -239,10 +312,17 @@ namespace ICSharpCode.Decompiler.CSharp case "EqualityContract": return IsGeneratedEqualityContract(property); default: - return false; + return IsPropertyDeclaredByPrimaryConstructor(property); } } + public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property) + { + var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); + return primaryCtor != null + && autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst)); + } + private bool IsGeneratedCopyConstructor(IMethod method) { /* @@ -779,18 +859,18 @@ namespace ICSharpCode.Decompiler.CSharp bool Visit(ILInstruction inst) { if (inst is BinaryNumericInstruction - { - Operator: BinaryNumericOperator.Add, - CheckForOverflow: false, - Left: BinaryNumericInstruction { - Operator: BinaryNumericOperator.Mul, + Operator: BinaryNumericOperator.Add, CheckForOverflow: false, - Left: var left, - Right: LdcI4 { Value: -1521134295 } - }, - Right: var right - }) + Left: BinaryNumericInstruction + { + Operator: BinaryNumericOperator.Mul, + CheckForOverflow: false, + Left: var left, + Right: LdcI4 { Value: -1521134295 } + }, + Right: var right + }) { if (!Visit(left)) return false; @@ -834,18 +914,70 @@ namespace ICSharpCode.Decompiler.CSharp } } + bool IsGeneratedDeconstruct(IMethod method) + { + Debug.Assert(method.Name == "Deconstruct" && method.Parameters.Count == primaryCtor.Parameters.Count); + + if (!method.ReturnType.IsKnownType(KnownTypeCode.Void)) + return false; + + for (int i = 0; i < method.Parameters.Count; i++) + { + var deconstruct = method.Parameters[i]; + var ctor = primaryCtor.Parameters[i]; + + if (!deconstruct.IsOut) + return false; + + if (!ctor.Type.Equals(((ByReferenceType)deconstruct.Type).ElementType)) + return false; + + if (ctor.Name != deconstruct.Name) + return false; + } + + var body = DecompileBody(method); + if (body == null || body.Instructions.Count != method.Parameters.Count + 1) + return false; + + for (int i = 0; i < body.Instructions.Count - 1; i++) + { + // stobj T(ldloc parameter, call getter(ldloc this)) + if (!body.Instructions[i].MatchStObj(out var targetInst, out var getter, out _)) + return false; + if (!targetInst.MatchLdLoc(out var target)) + return false; + if (!(target.Kind == VariableKind.Parameter && target.Index == i)) + return false; + + if (getter is not Call call || call.Arguments.Count != 1) + return false; + if (!call.Arguments[0].MatchLdThis()) + return false; + + if (!call.Method.IsAccessor) + return false; + var autoProperty = (IProperty)call.Method.AccessorOwner; + if (!autoPropertyToBackingField.ContainsKey(autoProperty)) + return false; + } + + var returnInst = body.Instructions.LastOrDefault(); + return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop(); + } + 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) + Method: + { + AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter, + AccessorOwner: IProperty property + } + } call) { if (call.Arguments.Count != 1) return false; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index e9b119bd8..41870ba48 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -117,6 +117,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax VisitChildren(functionPointerType); } + public virtual void VisitInvocationType(InvocationAstType invocationType) + { + VisitChildren(invocationType); + } + public virtual void VisitAttribute(Attribute attribute) { VisitChildren(attribute); @@ -790,6 +795,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren(functionPointerType); } + public virtual T VisitInvocationType(InvocationAstType invocationType) + { + return VisitChildren(invocationType); + } + public virtual T VisitAttribute(Attribute attribute) { return VisitChildren(attribute); @@ -1463,6 +1473,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren(functionPointerType, data); } + public virtual S VisitInvocationType(InvocationAstType invocationType, T data) + { + return VisitChildren(invocationType, data); + } + public virtual S VisitAttribute(Attribute attribute, T data) { return VisitChildren(attribute, data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs index 6a5bdfc75..baec135cf 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs @@ -107,6 +107,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax get { return GetChildrenByRole(Roles.BaseType); } } + public AstNodeCollection PrimaryConstructorParameters { + get { return GetChildrenByRole(Roles.Parameter); } + } + public AstNodeCollection Constraints { get { return GetChildrenByRole(Roles.Constraint); } } @@ -144,6 +148,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return o != null && this.ClassType == o.ClassType && MatchString(this.Name, o.Name) && this.MatchAttributesAndModifiers(o, match) && this.TypeParameters.DoMatch(o.TypeParameters, match) && this.BaseTypes.DoMatch(o.BaseTypes, match) && this.Constraints.DoMatch(o.Constraints, match) + && this.PrimaryConstructorParameters.DoMatch(o.PrimaryConstructorParameters, match) && this.Members.DoMatch(o.Members, match); } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs index e2954fcce..25fa54d02 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs @@ -142,6 +142,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax void VisitTupleType(TupleAstType tupleType); void VisitTupleTypeElement(TupleTypeElement tupleTypeElement); void VisitFunctionPointerType(FunctionPointerAstType functionPointerType); + void VisitInvocationType(InvocationAstType invocationType); void VisitComposedType(ComposedType composedType); void VisitArraySpecifier(ArraySpecifier arraySpecifier); void VisitPrimitiveType(PrimitiveType primitiveType); @@ -289,6 +290,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitTupleType(TupleAstType tupleType); S VisitTupleTypeElement(TupleTypeElement tupleTypeElement); S VisitFunctionPointerType(FunctionPointerAstType functionPointerType); + S VisitInvocationType(InvocationAstType invocationType); S VisitComposedType(ComposedType composedType); S VisitArraySpecifier(ArraySpecifier arraySpecifier); S VisitPrimitiveType(PrimitiveType primitiveType); @@ -436,6 +438,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitTupleType(TupleAstType tupleType, T data); S VisitTupleTypeElement(TupleTypeElement tupleTypeElement, T data); S VisitFunctionPointerType(FunctionPointerAstType functionPointerType, T data); + S VisitInvocationType(InvocationAstType invocationType, T data); S VisitComposedType(ComposedType composedType, T data); S VisitArraySpecifier(ArraySpecifier arraySpecifier, T data); S VisitPrimitiveType(PrimitiveType primitiveType, T data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/InvocationAstType.cs b/ICSharpCode.Decompiler/CSharp/Syntax/InvocationAstType.cs new file mode 100644 index 000000000..5dddbfb56 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/InvocationAstType.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2021 Siegfried Pammer +// +// 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.Text; + +using ICSharpCode.Decompiler.CSharp.Resolver; +using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + /// + /// BaseType "(" Argument { "," Argument } ")" + /// + public class InvocationAstType : AstType + { + public AstNodeCollection Arguments { + get { return GetChildrenByRole(Roles.Expression); } + } + + public AstType BaseType { + get { return GetChildByRole(Roles.Type); } + set { SetChildByRole(Roles.Type, value); } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitInvocationType(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitInvocationType(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitInvocationType(this, data); + } + + protected internal override bool DoMatch(AstNode other, Match match) + { + return other is InvocationAstType o + && this.BaseType.DoMatch(o.BaseType, match) + && this.Arguments.DoMatch(o.Arguments, match); + } + + public override ITypeReference ToTypeReference(NameLookupMode lookupMode, InterningProvider interningProvider = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index 85fe07b43..3335ff166 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -108,6 +108,22 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // Remove the statement: stmt.Remove(); break; + default: + return; + } + if (context.DecompileRun.RecordDecompilers.TryGetValue(currentCtor.DeclaringTypeDefinition, out var record) + && currentCtor.Equals(record.PrimaryConstructor) + && ci.ConstructorInitializerType == ConstructorInitializerType.Base) + { + if (constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl) + { + var baseType = typeDecl.BaseTypes.First(); + var newBaseType = new InvocationAstType(); + baseType.ReplaceWith(newBaseType); + newBaseType.BaseType = baseType; + ci.Arguments.MoveTo(newBaseType.Arguments); + } + constructorDeclaration.Remove(); } } @@ -154,6 +170,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms bool ctorIsUnsafe = instanceCtorsNotChainingWithThis.All(c => c.HasModifier(Modifiers.Unsafe)); + if (!context.DecompileRun.RecordDecompilers.TryGetValue(ctorMethodDef.DeclaringTypeDefinition, out var record)) + record = null; + // Recognize field or property initializers: // Translate first statement in all ctors (if all ctors have the same statement) into an initializer. bool allSame; @@ -166,14 +185,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (!(fieldOrPropertyOrEvent is IField) && !(fieldOrPropertyOrEvent is IProperty) && !(fieldOrPropertyOrEvent is IEvent)) break; var fieldOrPropertyOrEventDecl = members.FirstOrDefault(f => f.GetSymbol() == fieldOrPropertyOrEvent) as EntityDeclaration; - // Cannot transform if member is not found or if it is a custom event. - if (fieldOrPropertyOrEventDecl == null || fieldOrPropertyOrEventDecl is CustomEventDeclaration) + // Cannot transform if it is a custom event. + if (fieldOrPropertyOrEventDecl is CustomEventDeclaration) break; + + Expression initializer = m.Get("initializer").Single(); // 'this'/'base' cannot be used in initializers if (initializer.DescendantsAndSelf.Any(n => n is ThisReferenceExpression || n is BaseReferenceExpression)) break; + if (initializer.Annotation()?.Variable.Kind == IL.VariableKind.Parameter) + { + // remove record ctor parameter assignments + if (IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) + initializer.Remove(); + else + break; + } + else + { + // cannot transform if member is not found + if (fieldOrPropertyOrEventDecl == null) + break; + } + allSame = true; for (int i = 1; i < instanceCtorsNotChainingWithThis.Length; i++) { @@ -193,6 +229,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { foreach (var ctor in instanceCtorsNotChainingWithThis) ctor.Body.First().Remove(); + if (fieldOrPropertyOrEventDecl == null) + continue; if (ctorIsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer)) { fieldOrPropertyOrEventDecl.Modifiers |= Modifiers.Unsafe; @@ -210,6 +248,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } } + bool IsPropertyDeclaredByPrimaryCtor(IProperty p, RecordDecompiler record) + { + if (p == null || record == null) + return false; + return record.IsPropertyDeclaredByPrimaryConstructor(p); + } + void RemoveSingleEmptyConstructor(IEnumerable members, ITypeDefinition contextTypeDefinition) { // if we're outside of a type definition skip this altogether diff --git a/ICSharpCode.Decompiler/DecompileRun.cs b/ICSharpCode.Decompiler/DecompileRun.cs index c917f1a6a..cb9b0f996 100644 --- a/ICSharpCode.Decompiler/DecompileRun.cs +++ b/ICSharpCode.Decompiler/DecompileRun.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.Documentation; using ICSharpCode.Decompiler.TypeSystem; @@ -16,6 +17,7 @@ namespace ICSharpCode.Decompiler public CancellationToken CancellationToken { get; set; } public DecompilerSettings Settings { get; } public IDocumentationProvider DocumentationProvider { get; set; } + public Dictionary RecordDecompilers { get; } = new Dictionary(); Lazy usingScope => new Lazy(() => CreateUsingScope(Namespaces)); diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 10444b671..d8c8b700c 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -137,6 +137,7 @@ namespace ICSharpCode.Decompiler forEachWithGetEnumeratorExtension = false; recordClasses = false; withExpressions = false; + usePrimaryConstructorSyntax = false; } } @@ -244,6 +245,24 @@ namespace ICSharpCode.Decompiler } } + bool usePrimaryConstructorSyntax = true; + + /// + /// Use primary constructor syntax with records. + /// + [Category("C# 9.0 / VS 2019.8")] + [Description("DecompilerSettings.UsePrimaryConstructorSyntax")] + public bool UsePrimaryConstructorSyntax { + get { return usePrimaryConstructorSyntax; } + set { + if (usePrimaryConstructorSyntax != value) + { + usePrimaryConstructorSyntax = value; + OnPropertyChanged(); + } + } + } + bool functionPointers = true; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 4c06d9e68..f752848ec 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -75,6 +75,7 @@ + diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index cf17ec058..5702efa5f 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1262,6 +1262,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Use primary constructor syntax with records. + /// + public static string DecompilerSettings_UsePrimaryConstructorSyntax { + get { + return ResourceManager.GetString("DecompilerSettings.UsePrimaryConstructorSyntax", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use ref locals to accurately represent order of evaluation. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 45c9da4f3..56be5cf81 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -450,6 +450,9 @@ Are you sure you want to continue? Use pattern-based fixed statement + + Use primary constructor syntax with records + Use ref locals to accurately represent order of evaluation