Browse Source

Many thanks to @sonyps5201314 for providing the test cases and a suggested fix, which inspired these changes.

Various improvements regarding primary constructor decompilation, including:
- introduce `HasPrimaryConstructor` property in the AST, as there is a difference between no primary constructor and a parameterless primary constructor
- improved support for inherited records and forwarded ctor calls
- exclude non-public fields and properties in IsPrintedMember
- introduce an option to always make the decompiler emit primary constructors, when possible
pull/3613/head
Siegfried Pammer 2 months ago
parent
commit
9c8d1e48d9
  1. 2
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 16
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 12
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs
  4. 416
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Playstation.cs
  5. 307
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/PlaystationPreferPrimary.cs
  6. 3
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Structs.cs
  7. 2
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  8. 2
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
  9. 144
      ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
  10. 3
      ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs
  11. 66
      ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
  12. 39
      ICSharpCode.Decompiler/DecompilerSettings.cs

2
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -163,6 +163,8 @@ @@ -163,6 +163,8 @@
<Compile Include="TestCases\Pretty\Issue3571_B.cs" />
<Compile Include="TestCases\Pretty\Issue3571_A.cs" />
<Compile Include="TestCases\Pretty\Issue3576.cs" />
<Compile Include="TestCases\Pretty\PlaystationPreferPrimary.cs" />
<Compile Include="TestCases\Pretty\Playstation.cs" />
<None Include="TestCases\Ugly\NoLocalFunctions.Expected.cs" />
<None Include="TestCases\ILPretty\Issue3504.cs" />
<Compile Include="TestCases\ILPretty\MonoFixed.cs" />

16
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -535,6 +535,22 @@ namespace ICSharpCode.Decompiler.Tests @@ -535,6 +535,22 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable);
}
[Test]
public async Task Playstation([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
// see https://github.com/icsharpcode/ILSpy/pull/3598#issuecomment-3465151525
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable);
}
[Test]
public async Task PlaystationPreferPrimary([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
// see https://github.com/icsharpcode/ILSpy/pull/3598#issuecomment-3465151525
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable, configureDecompiler: settings => {
settings.PreferPrimaryConstructorIfPossible = true;
});
}
[Test]
public async Task ExtensionProperties([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{

12
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs

@ -127,6 +127,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -127,6 +127,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
public class ClassWithPrimaryCtorUsingGlobalParameterInExpressionAssignedToProperty(int a)
{
public int A { get; set; } = (int)Math.Abs(Math.PI * (double)a);
public void Print()
{
Console.WriteLine(A);
}
}
public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToEvent(EventHandler a)
{
public event EventHandler A = a;

416
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Playstation.cs

@ -0,0 +1,416 @@ @@ -0,0 +1,416 @@
using System;
using System.Diagnostics;
using System.Threading;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation
{
public record struct CopilotContextId
{
public Guid Id { get; }
public CopilotContextId()
{
Id = Guid.NewGuid();
}
public CopilotContextId(Guid id)
{
Id = id;
}
}
public class CopilotContextId_Class(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_Class(Guid id, int value)
: this(Guid.NewGuid())
{
}
public CopilotContextId_Class()
: this(Guid.NewGuid(), 222)
{
}
}
public record CopilotContextId_RecordClass(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_RecordClass()
: this(Guid.NewGuid())
{
}
}
public record struct CopilotContextId_RecordStruct(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_RecordStruct()
: this(Guid.NewGuid())
{
}
}
public struct CopilotContextId_Struct
{
public Guid guid { get; }
public CopilotContextId_Struct(Guid id)
{
guid = id;
}
public CopilotContextId_Struct()
: this(Guid.NewGuid())
{
}
}
public abstract record CopilotQueriedMention
{
public abstract ConsoleKey Type { get; }
public string DisplayName { get; init; }
public string FullName { get; init; }
public object ProviderMoniker { get; init; }
internal CopilotQueriedMention(object providerMoniker, string fullName, string displayName)
{
ProviderMoniker = providerMoniker;
FullName = fullName;
DisplayName = displayName;
}
}
public record CopilotQueriedScopeMention : CopilotQueriedMention
{
public override ConsoleKey Type { get; } = ConsoleKey.Enter;
public CopilotQueriedScopeMention(object providerMoniker, string fullName, string displayName)
: base(providerMoniker, fullName, displayName)
{
}
}
public class DeserializationException(string response, Exception innerException) : Exception("Error occured while deserializing the response", innerException)
{
public string Response { get; } = response;
}
internal static class Ensure
{
public static T NotNull<T>(T? value, string name)
{
if (value == null)
{
throw new ArgumentNullException(name);
}
return value;
}
public static string NotEmptyString(object? value, string name)
{
#if OPT
string obj = (value as string) ?? value?.ToString();
if (obj == null)
{
throw new ArgumentNullException(name);
}
if (string.IsNullOrWhiteSpace(obj))
{
throw new ArgumentException("Parameter cannot be an empty string", name);
}
return obj;
#else
string text = (value as string) ?? value?.ToString();
if (text == null)
{
throw new ArgumentNullException(name);
}
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Parameter cannot be an empty string", name);
}
return text;
#endif
}
}
public struct FromBinaryOperator
{
public int Leet;
public FromBinaryOperator(int dummy1, int dummy2)
{
Leet = dummy1 + dummy2;
}
}
public struct FromCall
{
public int Leet;
public FromCall(int dummy1, int dummy2)
{
Leet = Math.Max(dummy1, dummy2);
}
}
public struct FromConvert
{
public int Leet;
public FromConvert(double dummy1, double dummy2)
{
Leet = (int)Math.Min(dummy1, dummy2);
}
}
public record NamedParameter(string name, object? value, bool encode = true) : Parameter(Ensure.NotEmptyString(name, "name"), value, encode);
[DebuggerDisplay("{DebuggerDisplay()}")]
public abstract record Parameter
{
public string? Name { get; }
public object? Value { get; }
public bool Encode { get; }
protected virtual string ValueString => Value?.ToString() ?? "null";
protected Parameter(string? name, object? value, bool encode)
{
Name = name;
Value = value;
Encode = encode;
}
public sealed override string ToString()
{
#if OPT
if (Value != null)
{
return Name + "=" + ValueString;
}
return Name ?? "";
#else
return (Value == null) ? (Name ?? "") : (Name + "=" + ValueString);
#endif
}
protected string DebuggerDisplay()
{
return GetType().Name.Replace("Parameter", "") + " " + ToString();
}
}
public class Person(string name, int age)
{
private readonly string _name = name;
private readonly int _age = age;
public string Email { get; init; }
public Person(string name, int age, string email)
: this(name, age)
{
if (string.IsNullOrEmpty(email))
{
throw new ArgumentException("Email cannot be empty");
}
Email = email;
Console.WriteLine("Created person: " + name);
}
}
public class PersonPrimary(string name, int age)
{
private readonly string _name = name;
}
public class PersonPrimary_CaptureParams(string name, int age)
{
public string GetDetails()
{
return $"{name}, {age}";
}
}
public class PersonRegular1
{
private readonly string _name = "name";
private readonly int _age = 23;
public PersonRegular1(string name, int age)
{
Thread.Sleep(1000);
_age = name.Length;
}
}
public class PersonRegular2
{
private readonly string _name = "name" + Environment.GetEnvironmentVariable("Path");
private readonly int _age = Environment.GetEnvironmentVariable("Path")?.Length ?? (-1);
public PersonRegular2(string name, int age)
{
}
}
public record QueryParameter(string name, object? value, bool encode = true) : NamedParameter(name, value, encode);
internal ref struct RefFields
{
public ref int Field0;
public RefFields(ref int v)
{
Field0 = ref v;
}
}
internal struct StructWithDefaultCtor
{
private int X = 42;
public StructWithDefaultCtor()
{
}
}
internal struct ValueFields
{
public int Field0;
public ValueFields(int v)
{
Field0 = v;
}
}
internal class WebPair1(string name)
{
public string Name { get; } = name;
}
internal class WebPair1Primary
{
public string Name { get; }
public WebPair1Primary(string name)
{
Name = name;
}
}
internal class WebPair2
{
public string Name { get; }
public WebPair2(string name, string? value, ref readonly object encode)
{
Name = name;
}
}
internal class WebPair2Primary(string name, string? value, ref readonly object encode)
{
public string Name { get; } = name;
}
internal class WebPair3
{
public string Name { get; }
public string? Value { get; }
private string? WebValue { get; }
public WebPair3(string name, string? value, bool encode = false)
{
Name = name;
Value = value;
WebValue = (encode ? "111" : value);
}
}
internal class WebPair3Primary(string name, string? value, bool encode = false)
{
public string Name { get; } = name;
public string? Value { get; } = value;
private string? WebValue { get; } = encode ? "111" : value;
}
internal class WebPair4
{
public string Name { get; }
public string? Value { get; }
private string? WebValue { get; }
private string? WebValue2 { get; }
public WebPair4(string name, string? value, ref readonly object encode)
{
Name = name;
Value = value;
WebValue = ((encode == null) ? "111" : value);
WebValue2 = encode.ToString();
}
}
internal class WebPair4Primary(string name, string? value, ref readonly object encode)
{
public string Name { get; } = name;
public string? Value { get; } = value;
private string? WebValue { get; } = (encode == null) ? "111" : value;
private string? WebValue2 { get; } = encode.ToString();
}
internal class WebPair5
{
public string Name { get; }
public WebPair5(string name, string? value)
{
Name = name;
}
}
internal class WebPair5Primary(string name, string? value)
{
public string Name { get; } = name;
}
internal class WebPair6
{
public string? Value { get; }
public string Name { get; }
private string? WebValue { get; }
private string? WebValue2 { get; }
public WebPair6(string name, string? value, ref readonly object encode)
{
Value = name;
Name = value;
WebValue = ((name != null) ? "111" : value);
WebValue2 = ((value != null) ? name : "222");
}
}
internal class WebPair6Primary(string name, string? value, ref readonly object encode)
{
public string? Value { get; } = name;
public string Name { get; } = value;
private string? WebValue { get; } = (name != null) ? "111" : value;
private string? WebValue2 { get; } = (value != null) ? name : "222";
}
}

307
ICSharpCode.Decompiler.Tests/TestCases/Pretty/PlaystationPreferPrimary.cs

@ -0,0 +1,307 @@ @@ -0,0 +1,307 @@
using System;
using System.Diagnostics;
using System.Threading;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.PlaystationPreferPrimary
{
public record struct CopilotContextId
{
public Guid Id { get; }
public CopilotContextId()
{
Id = Guid.NewGuid();
}
public CopilotContextId(Guid id)
{
Id = id;
}
}
public class CopilotContextId_Class(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_Class(Guid id, int value)
: this(Guid.NewGuid())
{
}
public CopilotContextId_Class()
: this(Guid.NewGuid(), 222)
{
}
}
public record CopilotContextId_RecordClass(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_RecordClass()
: this(Guid.NewGuid())
{
}
}
public record struct CopilotContextId_RecordStruct(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_RecordStruct()
: this(Guid.NewGuid())
{
}
}
public struct CopilotContextId_Struct(Guid id)
{
public Guid guid { get; } = id;
public CopilotContextId_Struct()
: this(Guid.NewGuid())
{
}
}
public abstract record CopilotQueriedMention
{
public abstract ConsoleKey Type { get; }
public string DisplayName { get; init; }
public string FullName { get; init; }
public object ProviderMoniker { get; init; }
internal CopilotQueriedMention(object providerMoniker, string fullName, string displayName)
{
ProviderMoniker = providerMoniker;
FullName = fullName;
DisplayName = displayName;
}
}
public record CopilotQueriedScopeMention : CopilotQueriedMention
{
public override ConsoleKey Type { get; } = ConsoleKey.Enter;
public CopilotQueriedScopeMention(object providerMoniker, string fullName, string displayName)
: base(providerMoniker, fullName, displayName)
{
}
}
public class DeserializationException(string response, Exception innerException) : Exception("Error occured while deserializing the response", innerException)
{
public string Response { get; } = response;
}
internal static class Ensure
{
public static T NotNull<T>(T? value, string name)
{
if (value == null)
{
throw new ArgumentNullException(name);
}
return value;
}
public static string NotEmptyString(object? value, string name)
{
#if OPT
string obj = (value as string) ?? value?.ToString();
if (obj == null)
{
throw new ArgumentNullException(name);
}
if (string.IsNullOrWhiteSpace(obj))
{
throw new ArgumentException("Parameter cannot be an empty string", name);
}
return obj;
#else
string text = (value as string) ?? value?.ToString();
if (text == null)
{
throw new ArgumentNullException(name);
}
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Parameter cannot be an empty string", name);
}
return text;
#endif
}
}
public struct FromBinaryOperator(int dummy1, int dummy2)
{
public int Leet = dummy1 + dummy2;
}
public struct FromCall(int dummy1, int dummy2)
{
public int Leet = Math.Max(dummy1, dummy2);
}
public struct FromConvert(double dummy1, double dummy2)
{
public int Leet = (int)Math.Min(dummy1, dummy2);
}
public record NamedParameter(string name, object? value, bool encode = true) : Parameter(Ensure.NotEmptyString(name, "name"), value, encode);
[DebuggerDisplay("{DebuggerDisplay()}")]
public abstract record Parameter
{
public string? Name { get; }
public object? Value { get; }
public bool Encode { get; }
protected virtual string ValueString => Value?.ToString() ?? "null";
protected Parameter(string? name, object? value, bool encode)
{
Name = name;
Value = value;
Encode = encode;
}
public sealed override string ToString()
{
#if OPT
if (Value != null)
{
return Name + "=" + ValueString;
}
return Name ?? "";
#else
return (Value == null) ? (Name ?? "") : (Name + "=" + ValueString);
#endif
}
protected string DebuggerDisplay()
{
return GetType().Name.Replace("Parameter", "") + " " + ToString();
}
}
public class Person(string name, int age)
{
private readonly string _name = name;
private readonly int _age = age;
public string Email { get; init; }
public Person(string name, int age, string email)
: this(name, age)
{
if (string.IsNullOrEmpty(email))
{
throw new ArgumentException("Email cannot be empty");
}
Email = email;
Console.WriteLine("Created person: " + name);
}
}
public class PersonPrimary(string name, int age)
{
private readonly string _name = name;
}
public class PersonPrimary_CaptureParams(string name, int age)
{
public string GetDetails()
{
return $"{name}, {age}";
}
}
public class PersonRegular1
{
private readonly string _name = "name";
private readonly int _age = 23;
public PersonRegular1(string name, int age)
{
Thread.Sleep(1000);
_age = name.Length;
}
}
public class PersonRegular2
{
private readonly string _name = "name" + Environment.GetEnvironmentVariable("Path");
private readonly int _age = Environment.GetEnvironmentVariable("Path")?.Length ?? (-1);
public PersonRegular2(string name, int age)
{
}
}
public record QueryParameter(string name, object? value, bool encode = true) : NamedParameter(name, value, encode);
internal ref struct RefFields(ref int v)
{
public ref int Field0 = ref v;
}
internal struct StructWithDefaultCtor()
{
private int X = 42;
}
internal struct ValueFields(int v)
{
public int Field0 = v;
}
internal class WebPair1(string name)
{
public string Name { get; } = name;
}
internal class WebPair2(string name, string? value, ref readonly object encode)
{
public string Name { get; } = name;
}
internal class WebPair3(string name, string? value, bool encode = false)
{
public string Name { get; } = name;
public string? Value { get; } = value;
private string? WebValue { get; } = encode ? "111" : value;
}
internal class WebPair4(string name, string? value, ref readonly object encode)
{
public string Name { get; } = name;
public string? Value { get; } = value;
private string? WebValue { get; } = (encode == null) ? "111" : value;
private string? WebValue2 { get; } = encode.ToString();
}
internal class WebPair5(string name, string? value)
{
public string Name { get; } = name;
}
internal class WebPair6(string name, string? value, ref readonly object encode)
{
public string? Value { get; } = name;
public string Name { get; } = value;
private string? WebValue { get; } = (name != null) ? "111" : value;
private string? WebValue2 { get; } = (value != null) ? name : "222";
}
}

3
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Structs.cs

@ -43,11 +43,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -43,11 +43,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
#if CS100
public struct StructWithDefaultCtor
{
public int X;
public int X = 42;
public StructWithDefaultCtor()
{
X = 42;
}
}
#endif

2
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -1311,6 +1311,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1311,6 +1311,8 @@ namespace ICSharpCode.Decompiler.CSharp
if (recordDecompiler?.PrimaryConstructor != null)
{
typeDecl.HasPrimaryConstructor = recordDecompiler.PrimaryConstructor.Parameters.Any() || typeDef.Kind is TypeKind.Struct;
foreach (var p in recordDecompiler.PrimaryConstructor.Parameters)
{
ParameterDeclaration pd = typeSystemAstBuilder.ConvertParameter(p);

2
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs

@ -1635,7 +1635,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -1635,7 +1635,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
}
WriteIdentifier(typeDeclaration.NameToken);
WriteTypeParameters(typeDeclaration.TypeParameters);
if (typeDeclaration.PrimaryConstructorParameters.Count > 0)
if (typeDeclaration.HasPrimaryConstructor)
{
Space(policy.SpaceBeforeMethodDeclarationParentheses);
WriteCommaSeparatedListInParenthesis(typeDeclaration.PrimaryConstructorParameters, policy.SpaceWithinMethodDeclarationParentheses);

144
ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs

@ -26,6 +26,7 @@ using System.Threading; @@ -26,6 +26,7 @@ using System.Threading;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
namespace ICSharpCode.Decompiler.CSharp
{
@ -169,81 +170,147 @@ namespace ICSharpCode.Decompiler.CSharp @@ -169,81 +170,147 @@ namespace ICSharpCode.Decompiler.CSharp
{
if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes)
return null;
if (isStruct)
return null;
}
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
IMethod primaryCtor = null;
Dictionary<IMethod, IMethod> ctorCallChain = new Dictionary<IMethod, IMethod>();
foreach (var method in recordTypeDef.Methods)
{
cancellationToken.ThrowIfCancellationRequested();
// Only consider public instance constructors.
// This by definition excludes the copy constructor.
if (method.IsStatic || !method.IsConstructor)
continue;
var m = method.Specialize(subst);
if (IsPrimaryConstructor(m, method))
return method;
primaryCtorParameterToAutoPropertyOrBackingField.Clear();
autoPropertyOrBackingFieldToPrimaryCtorParameter.Clear();
var body = DecompileBody(method);
if (body == null)
continue;
if (primaryCtor == null && method.Accessibility == Accessibility.Public && IsPrimaryConstructorBody(m, method, body))
{
primaryCtor = method;
}
else if (!IsCopyConstructor(method))
{
IMethod calledCtor = FindChainedConstructor(body);
// if no chained constructor found, continue,
// if a chained to a constructor of a different type, give up
if (calledCtor != null && calledCtor.DeclaringTypeDefinition?.Equals(method.DeclaringTypeDefinition) != true)
return null;
ctorCallChain[method] = calledCtor;
}
if (primaryCtor == null)
{
primaryCtorParameterToAutoPropertyOrBackingField.Clear();
autoPropertyOrBackingFieldToPrimaryCtorParameter.Clear();
}
}
return null;
if (primaryCtor == null)
return null;
bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod)
foreach (var (source, target) in ctorCallChain)
{
var next = target;
while (next != null && !primaryCtor.Equals(next))
{
if (!ctorCallChain.TryGetValue(target, out next))
return null;
}
if (next == null)
return null;
}
return primaryCtor;
bool IsPrimaryConstructorBody(IMethod method, IMethod unspecializedMethod, Block body)
{
Debug.Assert(method.IsConstructor);
var body = DecompileBody(method);
if (body == null)
return false;
if (method.Parameters.Count == 0)
if (method.Parameters.Count == 0 && !settings.PreferPrimaryConstructorIfPossible)
return false;
var addonInst = isStruct ? 1 : 2;
if (body.Instructions.Count < method.Parameters.Count + addonInst)
return false;
bool referencesMembersDeclaredByPrimaryConstructor = false;
for (int i = 0; i < method.Parameters.Count; i++)
int offset = 0;
while (offset < body.Instructions.Count)
{
if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst))
return false;
if (!body.Instructions[offset].MatchStFld(out var target, out var field, out var valueInst))
break;
if (!target.MatchLdThis())
return false;
if (method.Parameters[i].ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly)
bool valueReferencesParameter;
if (recordTypeDef.IsRecord && offset < method.Parameters.Count)
{
if (!valueInst.MatchLdObj(out valueInst, out _))
if (method.Parameters[offset].ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly)
{
if (!valueInst.MatchLdObj(out valueInst, out _))
return false;
}
if (!valueInst.MatchLdLoc(out var value))
return false;
if (!(value.Kind == VariableKind.Parameter && value.Index == offset))
return false;
valueReferencesParameter = true;
}
else
{
valueReferencesParameter = valueInst.Descendants.Any(x => x.MatchLdLoc(out var v) && v.Kind == VariableKind.Parameter && v.Index == offset);
}
if (!valueInst.MatchLdLoc(out var value))
return false;
if (!(value.Kind == VariableKind.Parameter && value.Index == i))
return false;
IMember backingMember;
if (backingFieldToAutoProperty.TryGetValue(field, out var property))
{
backingMember = property;
referencesMembersDeclaredByPrimaryConstructor |= valueReferencesParameter && (recordTypeDef.IsRecord || recordTypeDef.IsReferenceType == true);
}
else if (!recordTypeDef.IsRecord)
else
{
backingMember = field;
referencesMembersDeclaredByPrimaryConstructor |= valueReferencesParameter && recordTypeDef.IsReferenceType == true;
}
else
if (offset < method.Parameters.Count)
{
return false;
primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[offset], backingMember);
autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, unspecializedMethod.Parameters[offset]);
}
primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[i], backingMember);
autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, unspecializedMethod.Parameters[i]);
offset++;
}
if (!isStruct)
{
var baseCtorCall = body.Instructions.SecondToLastOrDefault() as CallInstruction;
if (baseCtorCall == null)
if (body.Instructions.ElementAtOrDefault(offset) is not Call { Method: var baseCtor } call)
return false;
if (!baseCtor.IsConstructor)
return false;
referencesMembersDeclaredByPrimaryConstructor |= call.Descendants.Any(i => i.MatchLdLoc(out var p) && p.Kind == VariableKind.Parameter && p.Index > 0);
offset++;
}
var returnInst = body.Instructions.LastOrDefault();
return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
if (!settings.PreferPrimaryConstructorIfPossible && !referencesMembersDeclaredByPrimaryConstructor)
{
return false;
}
return offset + 1 == body.Instructions.Count
&& body.Instructions[^1].MatchReturn(out var retVal)
&& retVal.MatchNop();
}
IMethod FindChainedConstructor(Block body)
{
foreach (var inst in body.Instructions)
{
switch (inst)
{
case Call { Method.IsConstructor: true } call:
return call.Method;
case StObj { Target: var target, Value: NewObj { Method.IsConstructor: true } value } stObj when target.MatchLdLoc(out var v) && v.IsThis():
return value.Method;
}
}
return null;
}
}
@ -379,7 +446,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -379,7 +446,8 @@ namespace ICSharpCode.Decompiler.CSharp
internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter)
{
var member = primaryCtorParameterToAutoPropertyOrBackingField[parameter];
if (!primaryCtorParameterToAutoPropertyOrBackingField.TryGetValue(parameter, out IMember member))
return (null, null);
if (member is IField field)
return (null, field);
return ((IProperty)member, autoPropertyToBackingField[(IProperty)member]);
@ -661,6 +729,10 @@ namespace ICSharpCode.Decompiler.CSharp @@ -661,6 +729,10 @@ namespace ICSharpCode.Decompiler.CSharp
{
return false; // static fields/properties are not printed
}
if (member.Accessibility != Accessibility.Public)
{
return false; // non-public fields/properties are not printed
}
if (!isStruct && member.Name == "EqualityContract")
{
return false; // EqualityContract is never printed
@ -1095,7 +1167,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1095,7 +1167,7 @@ namespace ICSharpCode.Decompiler.CSharp
if (!call.Method.IsAccessor)
return false;
var autoProperty = (IProperty)call.Method.AccessorOwner;
if (!autoPropertyToBackingField.ContainsKey(autoProperty))
if (!IsInheritedRecord && !autoPropertyToBackingField.ContainsKey(autoProperty))
return false;
}

3
ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs

@ -112,6 +112,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -112,6 +112,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
get { return GetChildrenByRole(Roles.BaseType); }
}
public bool HasPrimaryConstructor { get; set; }
public AstNodeCollection<ParameterDeclaration> PrimaryConstructorParameters {
get { return GetChildrenByRole(Roles.Parameter); }
}
@ -153,6 +155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -153,6 +155,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.HasPrimaryConstructor == o.HasPrimaryConstructor
&& this.PrimaryConstructorParameters.DoMatch(o.PrimaryConstructorParameters, match)
&& this.Members.DoMatch(o.Members, match);
}

66
ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs

@ -23,6 +23,7 @@ using System.Reflection; @@ -23,6 +23,7 @@ using System.Reflection;
using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.Semantics;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
@ -49,8 +50,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -49,8 +50,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
// If we're viewing some set of members (fields are direct children of SyntaxTree),
// we also need to handle those:
HandleInstanceFieldInitializers(node.Children);
HandleStaticFieldInitializers(node.Children);
HandleInstanceFieldInitializers(node.Children, context.CurrentTypeDefinition);
HandleStaticFieldInitializers(node.Children, context.CurrentTypeDefinition);
node.AcceptVisitor(this);
@ -127,7 +128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -127,7 +128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (record.IsInheritedRecord &&
ci?.ConstructorInitializerType == ConstructorInitializerType.Base &&
constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl)
constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 }, HasPrimaryConstructor: true } typeDecl)
{
var baseType = typeDecl.BaseTypes.First();
var newBaseType = new InvocationAstType();
@ -160,33 +161,59 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -160,33 +161,59 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
};
static readonly AstNode thisCallPattern = new ExpressionStatement(new InvocationExpression(new MemberReferenceExpression(new ThisReferenceExpression(), ".ctor"), new Repeat(new AnyNode())));
static readonly AstNode thisCallPattern = new ExpressionStatement(
new Choice {
new InvocationExpression(new MemberReferenceExpression(new ThisReferenceExpression(), ".ctor"), new Repeat(new AnyNode())),
new AssignmentExpression(new ThisReferenceExpression(), new ObjectCreateExpression(new AnyNode("structType"), new Repeat(new AnyNode())))
}
);
public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration)
{
var currentTypeDefinition = (ITypeDefinition)typeDeclaration.GetSymbol();
// Handle initializers on instance fields
HandleInstanceFieldInitializers(typeDeclaration.Members);
HandleInstanceFieldInitializers(typeDeclaration.Members, currentTypeDefinition);
// Now convert base constructor calls to initializers:
base.VisitTypeDeclaration(typeDeclaration);
// Remove single empty constructor:
RemoveSingleEmptyConstructor(typeDeclaration.Members, (ITypeDefinition)typeDeclaration.GetSymbol());
RemoveSingleEmptyConstructor(typeDeclaration.Members, currentTypeDefinition);
// Handle initializers on static fields:
HandleStaticFieldInitializers(typeDeclaration.Members);
HandleStaticFieldInitializers(typeDeclaration.Members, currentTypeDefinition);
}
static bool ChainsWithThis(ConstructorDeclaration ctor)
{
var ctorMethod = ctor.GetSymbol() as IMethod;
var firstStmt = ctor.Body.Statements.FirstOrDefault();
var m = thisCallPattern.Match(firstStmt);
if (!m.Success || ctorMethod == null)
return false;
if (m.Has("structType"))
{
var type = m.Get<AstType>("structType").Single().GetSymbol();
return type.Equals(ctorMethod.DeclaringType);
}
return true;
}
void HandleInstanceFieldInitializers(IEnumerable<AstNode> members)
void HandleInstanceFieldInitializers(IEnumerable<AstNode> members, ITypeDefinition currentTypeDefinition)
{
if (currentTypeDefinition is { Kind: TypeKind.Struct, IsRecord: false }
&& !context.Settings.StructDefaultConstructorsAndFieldInitializers)
{
return;
}
var instanceCtors = members.OfType<ConstructorDeclaration>().Where(c => (c.Modifiers & Modifiers.Static) == 0).ToArray();
var instanceCtorsNotChainingWithThis = instanceCtors.Where(ctor => !thisCallPattern.IsMatch(ctor.Body.Statements.FirstOrDefault())).ToArray();
var instanceCtorsNotChainingWithThis = instanceCtors.Where(ctor => !ChainsWithThis(ctor)).ToArray();
if (instanceCtorsNotChainingWithThis.Length > 0)
{
var ctorMethodDef = instanceCtorsNotChainingWithThis[0].GetSymbol() as IMethod;
ITypeDefinition declaringTypeDefinition = ctorMethodDef?.DeclaringTypeDefinition;
if (ctorMethodDef != null && declaringTypeDefinition?.IsReferenceType == false && !declaringTypeDefinition.IsRecord)
return;
bool ctorIsUnsafe = instanceCtorsNotChainingWithThis.All(c => c.HasModifier(Modifiers.Unsafe));
@ -219,11 +246,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -219,11 +246,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (initializer.DescendantsAndSelf.Any(n => n is ThisReferenceExpression || n is BaseReferenceExpression))
break;
var v = initializer.Annotation<ILVariableResolveResult>()?.Variable;
if (v?.Kind == IL.VariableKind.Parameter)
if (v?.Kind == IL.VariableKind.Parameter && IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent, record))
{
// remove record ctor parameter assignments
if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent, record))
break;
isStructPrimaryCtor = true;
if (fieldOrPropertyOrEvent is IField f)
fieldToVariableMap.Add(f, v);
@ -236,6 +261,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -236,6 +261,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// or if this is a struct record, but not the primary ctor
if (declaringTypeDefinition.IsRecord && declaringTypeDefinition.IsReferenceType == false && !isStructPrimaryCtor)
break;
// or if this is not a primary constructor and the initializer contains any parameter reference, we have to abort
if (!ctorMethodDef.Equals(record?.PrimaryConstructor))
{
bool referencesParameter = initializer.Annotation<ILInstruction>()?.Descendants
.OfType<IInstructionWithVariableOperand>()
.Any(inst => inst.Variable.Kind == VariableKind.Parameter) ?? false;
if (referencesParameter)
break;
}
}
allSame = true;
@ -298,6 +332,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -298,6 +332,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// if we're outside of a type definition skip this altogether
if (contextTypeDefinition == null)
return;
if (contextTypeDefinition.Kind == TypeKind.Struct)
return;
// first get non-static constructor declarations from the AST
var instanceCtors = members.OfType<ConstructorDeclaration>().Where(c => (c.Modifiers & Modifiers.Static) == 0).ToArray();
// if there's exactly one ctor and it's part of a type declaration or there's more than one member in the current selection
@ -323,7 +359,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -323,7 +359,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
}
void HandleStaticFieldInitializers(IEnumerable<AstNode> members)
void HandleStaticFieldInitializers(IEnumerable<AstNode> members, ITypeDefinition currentTypeDefinition)
{
// Translate static constructor into field initializers if the class is BeforeFieldInit
var staticCtor = members.OfType<ConstructorDeclaration>().FirstOrDefault(c => (c.Modifiers & Modifiers.Static) == Modifiers.Static);

39
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -151,6 +151,7 @@ namespace ICSharpCode.Decompiler @@ -151,6 +151,7 @@ namespace ICSharpCode.Decompiler
{
fileScopedNamespaces = false;
recordStructs = false;
structDefaultConstructorsAndFieldInitializers = false;
}
if (languageVersion < CSharp.LanguageVersion.CSharp11_0)
{
@ -187,7 +188,7 @@ namespace ICSharpCode.Decompiler @@ -187,7 +188,7 @@ namespace ICSharpCode.Decompiler
return CSharp.LanguageVersion.CSharp12_0;
if (scopedRef || requiredMembers || numericIntPtr || utf8StringLiterals || unsignedRightShift || checkedOperators)
return CSharp.LanguageVersion.CSharp11_0;
if (fileScopedNamespaces || recordStructs)
if (fileScopedNamespaces || recordStructs || structDefaultConstructorsAndFieldInitializers)
return CSharp.LanguageVersion.CSharp10_0;
if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension
|| recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns
@ -330,6 +331,24 @@ namespace ICSharpCode.Decompiler @@ -330,6 +331,24 @@ namespace ICSharpCode.Decompiler
}
}
bool structDefaultConstructorsAndFieldInitializers = true;
/// <summary>
/// Use field initializers in structs.
/// </summary>
[Category("C# 10.0 / VS 2022")]
[Description("DecompilerSettings.StructDefaultConstructorsAndFieldInitializers")]
public bool StructDefaultConstructorsAndFieldInitializers {
get { return structDefaultConstructorsAndFieldInitializers; }
set {
if (structDefaultConstructorsAndFieldInitializers != value)
{
structDefaultConstructorsAndFieldInitializers = value;
OnPropertyChanged();
}
}
}
bool withExpressions = true;
/// <summary>
@ -2124,6 +2143,24 @@ namespace ICSharpCode.Decompiler @@ -2124,6 +2143,24 @@ namespace ICSharpCode.Decompiler
}
}
bool preferPrimaryConstructorIfPossible = false;
/// <summary>
/// Prefer primary constructor syntax with classes and structs whenever possible.
/// </summary>
[Category("DecompilerSettings.Other")]
[Description("DecompilerSettings.PreferPrimaryConstructorIfPossible")]
public bool PreferPrimaryConstructorIfPossible {
get { return preferPrimaryConstructorIfPossible; }
set {
if (preferPrimaryConstructorIfPossible != value)
{
preferPrimaryConstructorIfPossible = value;
OnPropertyChanged();
}
}
}
bool inlineArrays = true;
/// <summary>

Loading…
Cancel
Save