Browse Source

Rewrite TransformFieldAndConstructorInitializers from a step-by-step AST-based analysis to an analysis that tracks the whole constructor body.

pull/3614/head
Siegfried Pammer 2 months ago
parent
commit
94050a4aca
  1. 18
      ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
  2. 6
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  3. 26
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  4. 2
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs
  5. 2
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs
  6. 1
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/.gitignore
  7. 12
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/CS73_StackAllocInitializers.cs
  8. 108
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs
  9. 8
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomShortCircuitOperators.cs
  10. 5
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs
  11. 47
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3452.cs
  12. 172
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3598.cs
  13. 71
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3610.cs
  14. 64
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3611.cs
  15. 421
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/PlaystationPreferPrimary.cs
  16. 177
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
  17. 6
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/StaticAbstractInterfaceMembers.cs
  18. 8
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/UnsafeCode.cs
  19. 51
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  20. 238
      ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
  21. 1061
      ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
  22. 20
      ICSharpCode.Decompiler/DecompilerSettings.cs
  23. 11
      ILSpy/Properties/Resources.Designer.cs
  24. 5
      ILSpy/Properties/Resources.resx
  25. 4
      ILSpy/Properties/Resources.zh-Hans.resx

18
ICSharpCode.Decompiler.Tests/Helpers/Tester.cs

@ -35,6 +35,7 @@ using ICSharpCode.Decompiler.CSharp; @@ -35,6 +35,7 @@ using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.PdbProvider;
@ -70,6 +71,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers @@ -70,6 +71,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers
NullableEnable = 0x8000,
ReferenceUnsafe = 0x10000,
CheckForOverflowUnderflow = 0x20000,
ProcessXmlDoc = 0x40000,
UseMcsMask = UseMcs2_6_4 | UseMcs5_23,
UseRoslynMask = UseRoslyn1_3_2 | UseRoslyn2_10_0 | UseRoslyn3_11_0 | UseRoslynLatest
}
@ -560,6 +562,8 @@ namespace System.Runtime.CompilerServices @@ -560,6 +562,8 @@ namespace System.Runtime.CompilerServices
$"-langversion:{languageVersion} " +
$"-unsafe -o{(flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ")}";
HashSet<string> noWarn = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// note: the /shared switch is undocumented. It allows us to use the VBCSCompiler.exe compiler
// server to speed up testing
if (roslynVersion != "legacy")
@ -601,6 +605,11 @@ namespace System.Runtime.CompilerServices @@ -601,6 +605,11 @@ namespace System.Runtime.CompilerServices
otherOptions += "-platform:anycpu ";
}
if (flags.HasFlag(CompilerOptions.ProcessXmlDoc))
{
otherOptions += $"-doc:\"{Path.ChangeExtension(results.PathToAssembly, ".xml")}\" ";
}
if (flags.HasFlag(CompilerOptions.CheckForOverflowUnderflow))
{
otherOptions += "-checked+ ";
@ -613,6 +622,12 @@ namespace System.Runtime.CompilerServices @@ -613,6 +622,12 @@ namespace System.Runtime.CompilerServices
if (preprocessorSymbols.Count > 0)
{
otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
noWarn.Add("CS1591");
}
if (noWarn.Count > 0)
{
otherOptions += " -nowarn:" + string.Join(",", noWarn);
}
var command = Cli.Wrap(cscPath)
@ -833,6 +848,9 @@ namespace System.Runtime.CompilerServices @@ -833,6 +848,9 @@ namespace System.Runtime.CompilerServices
var pdbFileName = Path.ChangeExtension(assemblyFileName, ".pdb");
if (File.Exists(pdbFileName))
decompiler.DebugInfoProvider = DebugInfoUtils.FromFile(module, pdbFileName);
var xmlDocFileName = Path.ChangeExtension(assemblyFileName, ".xml");
if (File.Exists(xmlDocFileName))
decompiler.DocumentationProvider = new XmlDocumentationProvider(xmlDocFileName);
var syntaxTree = decompiler.DecompileWholeModuleAsSingleFile(sortTypes: true);
StringWriter output = new StringWriter();

6
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -158,14 +158,16 @@ @@ -158,14 +158,16 @@
<Compile Include="TestCases\ILPretty\Issue3552.cs" />
<Compile Include="TestCases\Pretty\ExpandParamsArgumentsDisabled.cs" />
<Compile Include="TestCases\Pretty\ExtensionProperties.cs" />
<Compile Include="TestCases\Pretty\Issue3452.cs" />
<Compile Include="TestCases\Pretty\Issue3541.cs" />
<Compile Include="TestCases\Pretty\Issue3571_C.cs" />
<Compile Include="TestCases\Pretty\Issue3571_B.cs" />
<Compile Include="TestCases\Pretty\Issue3571_A.cs" />
<Compile Include="TestCases\Pretty\Issue3576.cs" />
<Compile Include="TestCases\Pretty\Issue3584.cs" />
<Compile Include="TestCases\Pretty\PlaystationPreferPrimary.cs" />
<Compile Include="TestCases\Pretty\Playstation.cs" />
<Compile Include="TestCases\Pretty\Issue3610.cs" />
<Compile Include="TestCases\Pretty\Issue3611.cs" />
<Compile Include="TestCases\Pretty\Issue3598.cs" />
<None Include="TestCases\Ugly\NoLocalFunctions.Expected.cs" />
<None Include="TestCases\ILPretty\Issue3504.cs" />
<Compile Include="TestCases\ILPretty\MonoFixed.cs" />

26
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -396,7 +396,7 @@ namespace ICSharpCode.Decompiler.Tests @@ -396,7 +396,7 @@ namespace ICSharpCode.Decompiler.Tests
[Test]
public async Task ConstructorInitializers([ValueSource(nameof(defaultOptionsWithMcs))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.ProcessXmlDoc);
}
[Test]
@ -532,23 +532,31 @@ namespace ICSharpCode.Decompiler.Tests @@ -532,23 +532,31 @@ namespace ICSharpCode.Decompiler.Tests
[Test]
public async Task Records([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable);
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task Issue3610([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task Playstation([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
public async Task Issue3611([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task Issue3452([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)
public async Task Issue3598([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;
});
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable);
}
[Test]

2
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Debug.cs

@ -50,7 +50,7 @@ public static class Program @@ -50,7 +50,7 @@ public static class Program
[Serializable]
[SpecialName]
[CompilationMapping(SourceConstructFlags.Closure)]
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>()
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>
{
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

2
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/FSharpLoops_Release.cs

@ -50,7 +50,7 @@ public static class Program @@ -50,7 +50,7 @@ public static class Program
[Serializable]
[SpecialName]
[CompilationMapping(SourceConstructFlags.Closure)]
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>()
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>
{
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

1
ICSharpCode.Decompiler.Tests/TestCases/Pretty/.gitignore vendored

@ -2,3 +2,4 @@ @@ -2,3 +2,4 @@
/*.dll
/*.exe
/*.pdb
/*.xml

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

@ -24,6 +24,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -24,6 +24,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class CS73_StackAllocInitializers
{
#if CS120
[StructLayout(LayoutKind.Sequential, Size = 5)]
private struct StructWithSize5(byte a, byte b, byte c, byte d, byte e)
{
public byte a = a;
public byte b = b;
public byte c = c;
public byte d = d;
public byte e = e;
}
#else
[StructLayout(LayoutKind.Sequential, Size = 5)]
private struct StructWithSize5
{
@ -42,6 +53,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -42,6 +53,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
this.e = e;
}
}
#endif
#if CS80
private class NestedContext1

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

@ -17,11 +17,40 @@ @@ -17,11 +17,40 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
public class ConstructorInitializers
{
public class JArray
{
private readonly List<object> objects = new List<object>();
private readonly List<string> strings = new List<string>();
public JArray()
{
}
public JArray(params object[] items)
{
foreach (object item in items)
{
objects.Add(item);
}
}
public JArray(object content)
{
objects.Add(content);
}
public JArray(string content)
{
strings.Add(content);
}
}
public struct Issue1743
{
public int Leet;
@ -38,6 +67,48 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -38,6 +67,48 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
#if CS120
public struct Issue1743WithPrimaryCtor(int dummy1, int dummy2)
{
public int Leet = dummy1 + dummy2;
public Issue1743WithPrimaryCtor(int dummy)
: this(dummy, dummy)
{
Leet += dummy;
}
}
/// <summary>
/// This is info about the class
/// </summary>
private struct StructWithXmlDocCtor
{
public int A;
public int B;
/// <summary>
/// This is info about the constructor
/// </summary>
public StructWithXmlDocCtor(int a, int b)
{
A = a;
B = b;
}
}
/// <summary>
/// This is info about the class
/// </summary>
private struct StructWithoutXmlDocCtor(int a, int b)
{
public int A = a;
public int B = b;
}
#endif
public class ClassWithConstant
{
// using decimal constants has the effect that there is a cctor
@ -97,11 +168,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -97,11 +168,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToField(int a)
{
private readonly int a = a;
#pragma warning disable CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
private readonly int _a = a;
#pragma warning restore CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
public void Print()
{
Console.WriteLine(a);
Console.WriteLine(_a);
}
}
@ -159,5 +232,36 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -159,5 +232,36 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
this.parent = parent;
}
}
#if CS100
public class PrimaryCtorClassThisChain(Guid id)
{
public Guid guid { get; } = id;
public PrimaryCtorClassThisChain(Guid id, int value)
: this(Guid.NewGuid())
{
}
public PrimaryCtorClassThisChain()
: this(Guid.NewGuid(), 222)
{
}
}
#if EXPECTED_OUTPUT
public class UnusedPrimaryCtorParameter
{
public UnusedPrimaryCtorParameter(int unused)
{
}
}
#else
public class UnusedPrimaryCtorParameter(int unused)
{
}
#endif
#endif
}
}

8
ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomShortCircuitOperators.cs

@ -128,6 +128,14 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.CustomShortCircuitOperat @@ -128,6 +128,14 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.CustomShortCircuitOperat
{
private readonly bool val;
public bool Val {
get {
return val;
}
set {
}
}
public S(bool val)
{
this.val = val;

5
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs

@ -93,6 +93,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.InitializerTests @@ -93,6 +93,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.InitializerTests
public int A;
public int B;
public int M()
{
return 42;
}
public S(int a)
{
A = a;

47
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3452.cs

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class Issue3452
{
private struct Data
{
public object Obj;
}
private class C1(object obj)
{
internal Data d = new Data {
Obj = obj
};
}
private class C2(object obj)
{
public object Obj => obj;
}
private class C3(StringComparison comparison)
{
private StringComparison _comparison = comparison;
internal StringComparison Test()
{
return comparison;
}
}
private struct S1(object obj)
{
internal Data d = new Data {
Obj = obj
};
}
private struct S2(object obj)
{
public object Obj => obj;
}
}
}

172
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Playstation.cs → ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3598.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
@ -150,154 +149,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation @@ -150,154 +149,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation
}
}
public struct FromBinaryOperator
// always use primary constructor because it is indistinguishable
public struct FromBinaryOperator(int dummy1, int dummy2)
{
public int Leet;
public FromBinaryOperator(int dummy1, int dummy2)
{
Leet = dummy1 + dummy2;
}
public int Leet = dummy1 + dummy2;
}
public struct FromCall
public struct FromCall(int dummy1, int dummy2)
{
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);
}
}
internal class Issue3452
{
private struct Data
{
public object Obj;
}
private class C1(object obj)
{
internal Data d = new Data {
Obj = obj
};
}
private class C2(object obj)
{
public object Obj => obj;
}
private class C3(StringComparison comparison)
{
private StringComparison _comparison = comparison;
internal StringComparison Test()
{
return comparison;
}
}
private struct S1
{
internal Data d;
public S1(object obj)
{
d = new Data {
Obj = obj
};
}
}
private struct S2(object obj)
{
public object Obj => obj;
}
public int Leet = Math.Max(dummy1, dummy2);
}
internal class Issue3610
public struct FromConvert(double dummy1, double dummy2)
{
private struct CtorDoubleAssignmentTest
{
#if EXPECTED_OUTPUT
public bool Value = false;
public CtorDoubleAssignmentTest(string arg1, int arg2)
{
Value = true;
}
#else
public bool Value;
public CtorDoubleAssignmentTest(string arg1, int arg2)
{
Value = false;
Value = true;
}
#endif
}
private struct CtorDoubleAssignmentTest2
{
public bool Value = true;
public CtorDoubleAssignmentTest2(string arg1, int arg2)
{
Value = false;
}
}
private class FieldInitTest
{
public bool Flag = true;
public Func<int, int> Action = (int a) => a;
public string Value;
public FieldInitTest(string value)
{
Value = value;
}
}
private abstract class PCFieldInitTest(StringComparison value)
{
private StringComparison _value = value;
public bool Func()
{
return value == StringComparison.Ordinal;
}
}
private class RecordTest<T>
{
private interface IInterface
{
T[] Objects { get; }
}
protected record Record(T[] Objects) : IInterface
{
public Record(List<T> objects)
: this(objects.ToArray())
{
}
}
}
private abstract record RecordTest2(Guid[] Guids);
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);
@ -386,6 +251,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation @@ -386,6 +251,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation
private readonly string _name = "name" + Environment.GetEnvironmentVariable("Path");
private readonly int _age = Environment.GetEnvironmentVariable("Path")?.Length ?? (-1);
private void Method()
{
Console.WriteLine("Hello");
}
public PersonRegular2(string name, int age)
{
}
@ -393,14 +263,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation @@ -393,14 +263,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation
public record QueryParameter(string name, object? value, bool encode = true) : NamedParameter(name, value, encode);
internal ref struct RefFields
internal ref struct RefFields(ref int v)
{
public ref int Field0;
public RefFields(ref int v)
{
Field0 = ref v;
}
public ref int Field0 = ref v;
}
internal struct StructWithDefaultCtor
@ -412,14 +277,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation @@ -412,14 +277,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Playstation
}
}
internal struct ValueFields
internal struct ValueFields(int v)
{
public int Field0;
public ValueFields(int v)
{
Field0 = v;
}
public int Field0 = v;
}
internal class WebPair1(string name)

71
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3610.cs

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class Issue3610
{
private struct CtorDoubleAssignmentTest
{
public bool Value;
public CtorDoubleAssignmentTest(string arg1, int arg2)
{
Value = false;
Value = true;
}
}
private struct CtorDoubleAssignmentTest2
{
public bool Value;
public CtorDoubleAssignmentTest2(string arg1, int arg2)
{
Value = true;
Value = false;
}
}
private class FieldInitTest
{
public bool Flag = true;
public Func<int, int> Action = (int a) => a;
public string Value;
public FieldInitTest(string value)
{
Value = value;
}
}
private abstract class PCFieldInitTest(StringComparison value)
{
private StringComparison _value = value;
public bool Func()
{
return value == StringComparison.Ordinal;
}
}
private class RecordTest<T>
{
private interface IInterface
{
T[] Objects { get; }
}
protected record Record(T[] Objects) : IInterface
{
public Record(List<T> objects)
: this(objects.ToArray())
{
}
}
}
private abstract record RecordTest2(Guid[] Guids);
}
}

64
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3611.cs

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class Issue3611
{
private class C4(string value)
{
public object Obj { get; } = new object();
public string Value { get; } = value;
}
//private class C5(C5.ValueArray array)
//{
// public struct ValueArray
// {
// private bool b;
// public bool[] ToArray()
// {
// return null;
// }
// }
// public bool[] Values = array.ToArray();
//}
private class BaseClass
{
protected BaseClass(int value)
{
}
}
//private class C6(C6.Data2 data) : BaseClass(data.Value)
//{
// public struct Data2 {
// public int Value { get; set; }
// }
// public Data2 Data => data;
//}
private struct S3<T>(T v)
{
public T Value => v;
}
private interface I1
{
int Number { get; }
}
// May be the same issue as S3
private struct S4<T>(int number) : IComparable<T> where T : I1
{
public int CompareTo(T other)
{
return number.CompareTo(other.Number);
}
}
}
}

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

@ -1,421 +0,0 @@ @@ -1,421 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.PlaystationPreferPrimary
{
#pragma warning disable CS0414, CS9113, CS9124
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);
}
internal class Issue3452
{
private struct Data
{
public object Obj;
}
private class C1(object obj)
{
internal Data d = new Data {
Obj = obj
};
}
private class C2(object obj)
{
public object Obj => obj;
}
private class C3(StringComparison comparison)
{
private StringComparison _comparison = comparison;
internal StringComparison Test()
{
return comparison;
}
}
private struct S1(object obj)
{
internal Data d = new Data {
Obj = obj
};
}
private struct S2(object obj)
{
public object Obj => obj;
}
}
internal class Issue3610
{
private struct CtorDoubleAssignmentTest
{
#if EXPECTED_OUTPUT
public bool Value = false;
public CtorDoubleAssignmentTest(string arg1, int arg2)
{
Value = true;
}
#else
public bool Value;
public CtorDoubleAssignmentTest(string arg1, int arg2)
{
Value = false;
Value = true;
}
#endif
}
private struct CtorDoubleAssignmentTest2
{
public bool Value = true;
public CtorDoubleAssignmentTest2(string arg1, int arg2)
{
Value = false;
}
}
private class FieldInitTest
{
public bool Flag = true;
public Func<int, int> Action = (int a) => a;
public string Value;
public FieldInitTest(string value)
{
Value = value;
}
}
private abstract class PCFieldInitTest(StringComparison value)
{
private StringComparison _value = value;
public bool Func()
{
return value == StringComparison.Ordinal;
}
}
private class RecordTest<T>
{
private interface IInterface
{
T[] Objects { get; }
}
protected record Record(T[] Objects) : IInterface
{
public Record(List<T> objects)
: this(objects.ToArray())
{
}
}
}
private abstract record RecordTest2(Guid[] Guids);
}
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(string name, int age)
{
private readonly string _name = "name" + Environment.GetEnvironmentVariable("Path");
private readonly int _age = Environment.GetEnvironmentVariable("Path")?.Length ?? (-1);
}
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";
}
}

177
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs

@ -128,6 +128,95 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -128,6 +128,95 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public abstract string AbstractProp { get; }
}
public abstract record BaseRecord
{
public string Name { get; }
public object Value { get; }
public bool Encode { get; }
protected BaseRecord(string name, object value, bool encode)
{
Name = name;
Value = value;
Encode = encode;
}
}
public record DerivedRecord(string name, object value, bool encode = true) : BaseRecord(string.IsNullOrEmpty(name) ? "name" : name, value, encode);
public record DefaultValuesRecord() : DerivedRecord("default", 42, encode: false);
public record RecordWithProtectedMember(int Value)
{
protected int Double => Value * 2;
}
public record InheritedRecordWithAdditionalMember(int Value) : RecordWithProtectedMember(Value)
{
public int MoreData { get; set; }
}
public record InheritedRecordWithAdditionalParameter(int Value, int Value2) : RecordWithProtectedMember(Value);
public record BaseWithString(string S);
public record DerivedWithAdditionalInt(int I) : BaseWithString(I.ToString());
public record DerivedWithNoAdditionalProperty(string S) : BaseWithString(S);
public record DerivedWithAdditionalProperty(string S2) : BaseWithString(S2);
public record DerivedWithAdditionalPropertyDifferentAccessor(string S) : BaseWithString(S)
{
public string S2 { get; set; } = S;
}
public record MultipleCtorsChainedNoPrimaryCtor
{
public int A { get; init; }
public string B { get; init; }
public double C { get; init; }
public MultipleCtorsChainedNoPrimaryCtor(double c)
{
A = 0;
B = null;
C = c;
}
public MultipleCtorsChainedNoPrimaryCtor(int a)
: this(3.14)
{
A = a;
}
public MultipleCtorsChainedNoPrimaryCtor(string b)
: this(4.13)
{
B = b;
}
}
public record UnexpectedCodeInCtor
{
public int A { get; init; }
public string B { get; init; }
public UnexpectedCodeInCtor(int A, string B)
{
this.A = A;
this.B = B;
Console.WriteLine();
}
public UnexpectedCodeInCtor(int A)
: this(A, null)
{
this.A = A;
}
}
}
#if CS100
@ -169,6 +258,49 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -169,6 +258,49 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public record struct PairWithPrimaryCtor<A, B>(A First, B Second);
public record struct PrimaryCtor(int A, string B);
public record struct MultipleCtorsNoPrimaryCtor
{
public Guid Id { get; }
public MultipleCtorsNoPrimaryCtor()
{
Id = Guid.NewGuid();
}
public MultipleCtorsNoPrimaryCtor(Guid id)
{
Id = id;
}
}
public record struct MultipleCtorsChainedNoPrimaryCtor
{
public int A { get; init; }
public string B { get; init; }
public double C { get; init; }
public MultipleCtorsChainedNoPrimaryCtor(double c)
{
A = 0;
B = null;
C = c;
}
public MultipleCtorsChainedNoPrimaryCtor(int a)
: this(3.14)
{
A = a;
}
public MultipleCtorsChainedNoPrimaryCtor(string b)
: this(4.13)
{
B = b;
}
}
public record struct PrimaryCtorWithAttribute([RecordTest("param")][property: RecordTest("property")][field: RecordTest("field")] int a);
public record struct PrimaryCtorWithField(int A, string B)
{
@ -201,6 +333,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -201,6 +333,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
public record struct PropertiesWithInitializers()
{
public int A { get; set; } = 41;
public int B { get; } = 42;
public int C => 43;
public object O { get; set; } = null;
public string S { get; set; } = "Hello";
public dynamic D { get; set; } = null;
}
[AttributeUsage(AttributeTargets.All)]
public class RecordTestAttribute : Attribute
{
@ -230,15 +372,34 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -230,15 +372,34 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
};
}
}
}
#endif
#if CS110
public record struct WithRequiredMembers
{
public int A { get; set; }
public required double B { get; set; }
public object C;
public required dynamic D;
public record struct WithRequiredMembers
{
public int A { get; set; }
public required double B { get; set; }
public object C;
public required dynamic D;
}
#endif
public record struct RecordWithMultipleCtors
{
public int A { get; set; }
public RecordWithMultipleCtors()
{
A = 42;
}
public RecordWithMultipleCtors(int A)
{
this.A = A;
}
public RecordWithMultipleCtors(string A)
{
this.A = int.Parse(A);
}
}
}
#endif
}

6
ICSharpCode.Decompiler.Tests/TestCases/Pretty/StaticAbstractInterfaceMembers.cs

@ -73,7 +73,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.StaticAbstractInterfaceM @@ -73,7 +73,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.StaticAbstractInterfaceM
internal interface IAmStatic<T> where T : IAmStatic<T>
{
static int f;
static int f = 42;
static T P { get; set; }
static event Action E;
static void M(object x)
@ -83,10 +83,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.StaticAbstractInterfaceM @@ -83,10 +83,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.StaticAbstractInterfaceM
{
throw new NotImplementedException();
}
static IAmStatic()
{
f = 42;
}
}
internal interface IAmVirtual<T> where T : IAmVirtual<T>

8
ICSharpCode.Decompiler.Tests/TestCases/Pretty/UnsafeCode.cs

@ -77,6 +77,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -77,6 +77,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public double Y;
}
#if CS120
public unsafe struct ResultStruct(byte* ptr1, byte* ptr2)
{
public unsafe byte* ptr1 = ptr1;
public unsafe byte* ptr2 = ptr2;
}
#else
public struct ResultStruct
{
public unsafe byte* ptr1;
@ -88,6 +95,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -88,6 +95,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
this.ptr2 = ptr2;
}
}
#endif
public struct StructWithFixedSizeMembers
{

51
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -1300,56 +1300,17 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1300,56 +1300,17 @@ namespace ICSharpCode.Decompiler.CSharp
// e.g. DelegateDeclaration
return entityDecl;
}
bool isRecordLike = typeDef.Kind switch {
TypeKind.Class => (settings.RecordClasses && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes,
TypeKind.Struct => (settings.RecordStructs && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes,
bool isRecord = typeDef.Kind switch {
TypeKind.Class => settings.RecordClasses && typeDef.IsRecord,
TypeKind.Struct => settings.RecordStructs && typeDef.IsRecord,
_ => false,
};
RecordDecompiler recordDecompiler = isRecordLike ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null;
RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null;
if (recordDecompiler != null)
decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler);
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);
(IProperty prop, IField field) = recordDecompiler.GetPropertyInfoByPrimaryConstructorParameter(p);
if (prop != null)
{
var 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);
}
}
if (field != null && (recordDecompiler.FieldIsGenerated(field) || typeDef.IsRecord))
{
var 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);
}
}
// With C# 9 records, the relative order of fields and properties matters:
IEnumerable<IMember> fieldsAndProperties = isRecordLike && typeDef.IsRecord
IEnumerable<IMember> fieldsAndProperties = typeDef.IsRecord
? recordDecompiler.FieldsAndProperties
: typeDef.Fields.Concat<IMember>(typeDef.Properties);
@ -1532,7 +1493,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1532,7 +1493,7 @@ namespace ICSharpCode.Decompiler.CSharp
{
return;
}
if (recordDecompiler?.FieldIsGenerated(field) == true)
if (TransformFieldAndConstructorInitializers.IsGeneratedPrimaryConstructorBackingField(field))
{
return;
}

238
ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs

@ -44,8 +44,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -44,8 +44,8 @@ namespace ICSharpCode.Decompiler.CSharp
readonly IType baseClass;
readonly Dictionary<IField, IProperty> backingFieldToAutoProperty = new Dictionary<IField, IProperty>();
readonly Dictionary<IProperty, IField> autoPropertyToBackingField = new Dictionary<IProperty, IField>();
readonly Dictionary<IParameter, IMember> primaryCtorParameterToAutoPropertyOrBackingField = new Dictionary<IParameter, IMember>();
readonly Dictionary<IMember, IParameter> autoPropertyOrBackingFieldToPrimaryCtorParameter = new Dictionary<IMember, IParameter>();
readonly Dictionary<IParameter, IProperty> primaryCtorParameterToAutoProperty = new Dictionary<IParameter, IProperty>();
readonly Dictionary<IProperty, IParameter> autoPropertyToPrimaryCtorParameter = new Dictionary<IProperty, IParameter>();
public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken)
{
@ -161,162 +161,124 @@ namespace ICSharpCode.Decompiler.CSharp @@ -161,162 +161,124 @@ namespace ICSharpCode.Decompiler.CSharp
IMethod DetectPrimaryConstructor()
{
if (recordTypeDef.IsRecord)
{
if (!settings.UsePrimaryConstructorSyntax)
return null;
}
else
{
if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes)
return null;
}
Debug.Assert(recordTypeDef.IsRecord);
if (!settings.UsePrimaryConstructorSyntax)
return null;
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
IMethod primaryCtor = null;
Dictionary<IMethod, IMethod> ctorCallChain = new Dictionary<IMethod, IMethod>();
Accessibility minAccessibility = recordTypeDef.IsAbstract ? Accessibility.Protected : Accessibility.Public;
IMethod guessedPrimaryCtor = null;
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);
var body = DecompileBody(method, allTransforms: method.Accessibility >= minAccessibility && !recordTypeDef.IsRecord);
if (body == null)
continue;
if (primaryCtor == null && method.Accessibility >= minAccessibility && IsPrimaryConstructorBody(m, method, body))
{
primaryCtor = method;
}
else if (!IsCopyConstructor(method))
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] = (IMethod)calledCtor?.MemberDefinition;
continue;
}
if (primaryCtor == null)
var m = method.Specialize(subst);
if (IsPrimaryConstructor(m, method))
{
primaryCtorParameterToAutoPropertyOrBackingField.Clear();
autoPropertyOrBackingFieldToPrimaryCtorParameter.Clear();
if (guessedPrimaryCtor == null)
{
guessedPrimaryCtor = method;
}
else
{
// Multiple primary constructor candidates found - give up
guessedPrimaryCtor = null;
break;
}
}
}
if (primaryCtor == null)
return null;
if (guessedPrimaryCtor == null)
{
primaryCtorParameterToAutoProperty.Clear();
}
foreach (var (source, target) in ctorCallChain)
foreach (var (parameter, property) in primaryCtorParameterToAutoProperty.ToArray())
{
var next = target;
while (next != null && !primaryCtor.Equals(next))
if (!parameter.Owner.Equals(guessedPrimaryCtor))
{
primaryCtorParameterToAutoProperty.Remove(parameter);
}
else
{
if (!ctorCallChain.TryGetValue(target, out next))
return null;
autoPropertyToPrimaryCtorParameter.Add(property, parameter);
}
if (next == null)
return null;
}
return primaryCtor;
return guessedPrimaryCtor;
bool IsPrimaryConstructorBody(IMethod method, IMethod unspecializedMethod, Block body)
bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod)
{
Debug.Assert(method.IsConstructor);
if (method.Parameters.Count == 0 && !settings.PreferPrimaryConstructorIfPossible)
var body = DecompileBody(method);
if (body == null)
return false;
bool referencesMembersDeclaredByPrimaryConstructor = false;
if (!isStruct)
{
if (body.Instructions.SecondToLastOrDefault() is not CallInstruction baseCtorCall)
return false;
var baseCtor = baseCtorCall.Method;
if (!baseCtor.IsConstructor)
return false;
if (baseCtor.DeclaringType.Equals(method.DeclaringType))
return false;
}
var addonInst = isStruct ? 1 : 2;
if (body.Instructions.Count < addonInst)
return false;
int offset = 0;
while (offset < body.Instructions.Count)
for (int i = 0; i < body.Instructions.Count - addonInst; i++)
{
if (!body.Instructions[offset].MatchStFld(out var target, out var field, out var valueInst))
break;
if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst))
return false;
if (!target.MatchLdThis())
return false;
bool valueReferencesParameter;
if (recordTypeDef.IsRecord && offset < method.Parameters.Count)
if (i < method.Parameters.Count)
{
if (method.Parameters[offset].ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly)
if (method.Parameters[i].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);
}
IMember backingMember;
if (backingFieldToAutoProperty.TryGetValue(field, out var property))
{
backingMember = property;
referencesMembersDeclaredByPrimaryConstructor |= valueReferencesParameter && (recordTypeDef.IsRecord || recordTypeDef.IsReferenceType == true);
}
else
{
backingMember = field;
referencesMembersDeclaredByPrimaryConstructor |= valueReferencesParameter && (FieldIsGenerated(field) || recordTypeDef.IsReferenceType == true);
}
if (offset < method.Parameters.Count)
{
if (primaryCtorParameterToAutoPropertyOrBackingField.ContainsKey(unspecializedMethod.Parameters[offset]))
return false;
if (autoPropertyOrBackingFieldToPrimaryCtorParameter.ContainsKey(backingMember))
if (!(value.Kind == VariableKind.Parameter && value.Index >= 0 && value.Index < method.Parameters.Count))
return false;
primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[offset], backingMember);
autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, unspecializedMethod.Parameters[offset]);
}
offset++;
}
if (!isStruct)
{
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++;
}
if (!backingFieldToAutoProperty.TryGetValue(field, out var property))
{
return false;
}
if (!settings.PreferPrimaryConstructorIfPossible && !referencesMembersDeclaredByPrimaryConstructor)
{
return false;
}
IParameter parameter = unspecializedMethod.Parameters[i];
if (primaryCtorParameterToAutoProperty.ContainsKey(parameter))
{
return false;
}
return offset + 1 == body.Instructions.Count
&& body.Instructions[^1].MatchReturn(out var retVal)
&& retVal.MatchNop();
}
if (recordTypeDef.Kind != TypeKind.Struct)
{
if (!(property.CanSet && property.Setter.IsInitOnly))
{
continue;
}
}
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;
primaryCtorParameterToAutoProperty.Add(parameter, property);
}
}
return null;
var returnInst = body.Instructions.LastOrDefault();
return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
}
}
@ -358,9 +320,6 @@ namespace ICSharpCode.Decompiler.CSharp @@ -358,9 +320,6 @@ namespace ICSharpCode.Decompiler.CSharp
/// </summary>
public bool MethodIsGenerated(IMethod method)
{
if (!recordTypeDef.IsRecord)
return false;
if (IsCopyConstructor(method))
{
return IsGeneratedCopyConstructor(method);
@ -432,36 +391,21 @@ namespace ICSharpCode.Decompiler.CSharp @@ -432,36 +391,21 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
internal bool FieldIsGenerated(IField field)
{
if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes)
return false;
var name = field.Name;
return name.StartsWith("<", StringComparison.Ordinal)
&& name.EndsWith(">P", StringComparison.Ordinal)
&& field.IsCompilerGenerated();
}
public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property)
{
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
return primaryCtor != null
&& autoPropertyOrBackingFieldToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
}
internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter 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]);
&& autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
}
internal IParameter GetPrimaryConstructorParameterFromBackingField(IField field)
internal Dictionary<IParameter, (IProperty, IField)> GetParameterToBackingStoreMap()
{
return autoPropertyOrBackingFieldToPrimaryCtorParameter[field];
var result = new Dictionary<IParameter, (IProperty, IField)>();
foreach (var (parameter, property) in primaryCtorParameterToAutoProperty)
{
result.Add(parameter, (property, autoPropertyToBackingField[property]));
}
return result;
}
public bool IsCopyConstructor(IMethod method)
@ -473,6 +417,9 @@ namespace ICSharpCode.Decompiler.CSharp @@ -473,6 +417,9 @@ namespace ICSharpCode.Decompiler.CSharp
return method.IsConstructor
&& method.Parameters.Count == 1
&& (recordTypeDef.IsSealed
? (method.Accessibility == Accessibility.Private)
: (method.Accessibility == Accessibility.Protected))
&& IsRecordType(method.Parameters[0].Type);
}
@ -1173,8 +1120,11 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1173,8 +1120,11 @@ namespace ICSharpCode.Decompiler.CSharp
if (!call.Method.IsAccessor)
return false;
var autoProperty = (IProperty)call.Method.AccessorOwner;
if (!IsInheritedRecord && !autoPropertyToBackingField.ContainsKey(autoProperty))
return false;
if (!autoPropertyToBackingField.ContainsKey(autoProperty))
{
if (autoProperty.DeclaringTypeDefinition == recordTypeDef)
return false;
}
}
var returnInst = body.Instructions.LastOrDefault();
@ -1212,7 +1162,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1212,7 +1162,7 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
Block DecompileBody(IMethod method, bool allTransforms = false)
Block DecompileBody(IMethod method)
{
if (method == null || method.MetadataToken.IsNil)
return null;
@ -1229,7 +1179,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1229,7 +1179,7 @@ namespace ICSharpCode.Decompiler.CSharp
var body = typeSystem.MainModule.MetadataFile.GetMethodBody(methodDef.RelativeVirtualAddress);
var ilReader = new ILReader(typeSystem.MainModule);
var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken);
var settings = allTransforms ? this.settings : new DecompilerSettings(LanguageVersion.CSharp1);
var settings = new DecompilerSettings(LanguageVersion.CSharp1);
var transforms = CSharpDecompiler.GetILTransforms();
// Remove the last couple transforms -- we don't need variable names etc. here
int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform);

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

File diff suppressed because it is too large Load Diff

20
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -2143,24 +2143,6 @@ namespace ICSharpCode.Decompiler @@ -2143,24 +2143,6 @@ 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>
@ -2315,7 +2297,7 @@ namespace ICSharpCode.Decompiler @@ -2315,7 +2297,7 @@ namespace ICSharpCode.Decompiler
/// <summary>
/// If set to false (the default), the decompiler will move field initializers at the start of constructors
/// to their respective field declrations (TransformFieldAndConstructorInitializers) only when the declaring
/// to their respective field declarations (TransformFieldAndConstructorInitializers) only when the declaring
/// type has BeforeFieldInit or the member IsConst.
/// If set true, the decompiler will always move them regardless of the flags.
/// </summary>

11
ILSpy/Properties/Resources.Designer.cs generated

@ -1406,6 +1406,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1406,6 +1406,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Use struct field initializers and default constructors.
/// </summary>
public static string DecompilerSettings_StructDefaultConstructorsAndFieldInitializers {
get {
return ResourceManager.GetString("DecompilerSettings.StructDefaultConstructorsAndFieldInitializers", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Switch expressions.
/// </summary>
@ -1579,7 +1588,7 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1579,7 +1588,7 @@ namespace ICSharpCode.ILSpy.Properties {
}
/// <summary>
/// Looks up a localized string similar to Use primary constructor syntax with records.
/// Looks up a localized string similar to Use primary constructor syntax with records.
/// </summary>
public static string DecompilerSettings_UsePrimaryConstructorSyntax {
get {

5
ILSpy/Properties/Resources.resx

@ -489,6 +489,9 @@ Are you sure you want to continue?</value> @@ -489,6 +489,9 @@ Are you sure you want to continue?</value>
<data name="DecompilerSettings.StringConcat" xml:space="preserve">
<value>Decompile 'string.Concat(a, b)' calls into 'a + b'</value>
</data>
<data name="DecompilerSettings.StructDefaultConstructorsAndFieldInitializers" xml:space="preserve">
<value>Use struct field initializers and default constructors</value>
</data>
<data name="DecompilerSettings.SwitchExpressions" xml:space="preserve">
<value>Switch expressions</value>
</data>
@ -547,7 +550,7 @@ Are you sure you want to continue?</value> @@ -547,7 +550,7 @@ Are you sure you want to continue?</value>
<value>Use primary constructor syntax for non-record types</value>
</data>
<data name="DecompilerSettings.UsePrimaryConstructorSyntax" xml:space="preserve">
<value>Use primary constructor syntax with records</value>
<value>Use primary constructor syntax with records</value>
</data>
<data name="DecompilerSettings.UsePrimaryConstructorSyntaxForNonRecordTypes" xml:space="preserve">
<value>Use primary constructor syntax for non-record types</value>

4
ILSpy/Properties/Resources.zh-Hans.resx

@ -117,6 +117,7 @@ @@ -117,6 +117,7 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="About" xml:space="preserve">
<value>关于</value>
</data>
@ -456,6 +457,9 @@ @@ -456,6 +457,9 @@
<data name="DecompilerSettings.StringConcat" xml:space="preserve">
<value>反编译 'string.Concat(a, b)' 调用为 'a + b'</value>
</data>
<data name="DecompilerSettings.StructDefaultConstructorsAndFieldInitializers" type="System.Resources.ResXNullRef, System.Windows.Forms">
<value />
</data>
<data name="DecompilerSettings.SwitchExpressions" xml:space="preserve">
<value>switch 表达式</value>
</data>

Loading…
Cancel
Save