diff --git a/ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs b/ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs index 2645e6f936..450d84e014 100644 --- a/ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs +++ b/ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs @@ -49,5 +49,12 @@ public void GetMinimumRequiredVersionReturnsTheHighestEnabledFeatureVersion() settings.ParamsCollections = false; Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp11_0)); } + + [Test] + public void CollectionExpressionsRequireCSharp12() + { + Assert.That(new DecompilerSettings(LanguageVersion.CSharp11_0).CollectionExpressions, Is.False); + Assert.That(new DecompilerSettings(LanguageVersion.CSharp12_0).CollectionExpressions, Is.True); + } } } diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 714454f75b..3f4399b760 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -230,6 +230,8 @@ + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 7e9f8df947..d3c50f2797 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -429,14 +429,33 @@ public async Task Issue3729() await Run(); } + [Test] + public async Task LateBaseConstructorCall() + { + await Run(); + } + + [Test] + public async Task CompilerGeneratedAutoProperty() + { + await Run(); + } + + [Test] + public async Task MissingBaseConstructor() + { + await Run(expectedText: "//IL_0001: Unknown result type (might be due to invalid IL or missing references)"); + } + async Task Run([CallerMemberName] string testName = null, DecompilerSettings settings = null, - AssemblerOptions assemblerOptions = AssemblerOptions.Library) + AssemblerOptions assemblerOptions = AssemblerOptions.Library, string expectedText = null) { if (settings == null) { // never use file-scoped namespaces, unless explicitly specified settings = new DecompilerSettings { FileScopedNamespaces = false }; } + settings.CollectionExpressions = false; var ilFile = Path.Combine(TestCasePath, testName + ".il"); var csFile = Path.Combine(TestCasePath, testName + ".cs"); @@ -444,6 +463,8 @@ async Task Run([CallerMemberName] string testName = null, DecompilerSettings set var decompiled = await Tester.DecompileCSharp(executable, settings).ConfigureAwait(false); CodeAssert.FilesAreEqual(csFile, decompiled, ["EXPECTED_OUTPUT"]); + if (expectedText != null) + Assert.That(File.ReadAllText(decompiled), Does.Contain(expectedText)); Tester.RepeatOnIOError(() => File.Delete(decompiled)); } diff --git a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs index cdffbe5020..5be0353008 100644 --- a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs @@ -23,6 +23,8 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using ICSharpCode.Decompiler.CSharp; @@ -58,6 +60,29 @@ public void HelloWorld() TestSequencePoints(); } + [Test] + public void GlobalNamespaceDocument() + { + (string peFileName, _) = CompileTestCase(nameof(GlobalNamespaceDocument)); + var module = new PEFile(peFileName); + var resolver = new UniversalAssemblyResolver(peFileName, false, + module.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); + var decompiler = new CSharpDecompiler(module, resolver, new DecompilerSettings()); + const string sourceText = "source text"; + using var generatedPdb = new MemoryStream(); + new PortablePdbWriter { + NoLogo = true, + SourceTextProvider = _ => sourceText, + }.WritePdb(module, decompiler, new DecompilerSettings(), generatedPdb); + + generatedPdb.Position = 0; + var reader = MetadataReaderProvider.FromPortablePdbStream(generatedPdb).GetMetadataReader(); + var document = reader.GetDocument(reader.Documents.Single()); + Assert.That(reader.GetString(document.Name), Is.EqualTo("GlobalNamespaceDocument.cs")); + Assert.That(reader.GetBlobBytes(document.Hash), + Is.EqualTo(SHA256.HashData(Encoding.UTF8.GetBytes(sourceText)))); + } + [Test] public void ForLoopTests() { diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 01b6228c96..cf1b9ae798 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -466,7 +466,8 @@ public async Task UnsafeCode([ValueSource(nameof(defaultOptions))] CompilerOptio [Test] public async Task ConstructorInitializers([ValueSource(nameof(defaultOptionsWithMcs))] CompilerOptions cscOptions) { - await RunForLibrary(cscOptions: cscOptions | CompilerOptions.ProcessXmlDoc); + await RunForLibrary(cscOptions: cscOptions | CompilerOptions.ProcessXmlDoc, + configureDecompiler: settings => settings.CollectionExpressions = Tester.GetPreprocessorSymbols(cscOptions).Contains("CS120")); } [Test] @@ -744,7 +745,8 @@ public async Task RefLocalsAndReturns([ValueSource(nameof(roslyn2OrNewerOptions) [Test] public async Task CachedReadOnlySpanInitialization([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions) { - await RunForLibrary(cscOptions: cscOptions); + await RunForLibrary(cscOptions: cscOptions, + configureDecompiler: settings => settings.CollectionExpressions = Tester.GetPreprocessorSymbols(cscOptions).Contains("CS120")); } [Test] @@ -1040,6 +1042,13 @@ public async Task InlineArrayTests([ValueSource(nameof(roslyn4OrNewerOptions))] await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task CollectionExpressions([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions, + configureDecompiler: settings => settings.CollectionExpressions = Tester.GetPreprocessorSymbols(cscOptions).Contains("CS120")); + } + [Test] public async Task Issue3684([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) { @@ -1080,6 +1089,7 @@ async Task Run([CallerMemberName] string testName = null, AssemblerOptions asmOp // 2. Decompile var settings = Tester.GetSettings(cscOptions); + settings.CollectionExpressions = false; configureDecompiler?.Invoke(settings); var decompiled = await Tester.DecompileCSharp(exeFile, settings).ConfigureAwait(false); diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs index 6391ca96ba..db82e48f83 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs @@ -17,6 +17,8 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.IO; +using System.Reflection; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; @@ -129,5 +131,63 @@ public void VerifyUniversalAssemblyResolverParseTargetFramework(string targetFra Assert.That(id, Is.EqualTo(identifier)); Assert.That(v.ToString(3), Is.EqualTo(version)); } + + [TestCase(TargetFrameworkIdentifier.NET, true)] + [TestCase(TargetFrameworkIdentifier.NETCoreApp, true)] + [TestCase(TargetFrameworkIdentifier.NETStandard, true)] + [TestCase(TargetFrameworkIdentifier.Silverlight, false)] + public void VerifyUseOfDotNetCorePathFinder(TargetFrameworkIdentifier identifier, bool expected) + { + Assert.That(UniversalAssemblyResolver.UsesDotNetCorePathFinder(identifier), Is.EqualTo(expected)); + } + [Test] + public void NetStandardResolvesSharedRuntimeAssembly() + { + var reference = AssemblyNameReference.Parse( + "System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"); + var resolver = new UniversalAssemblyResolver(null, false, ".NETStandard,Version=v2.0"); + + var file = resolver.FindAssemblyFile(reference); + + Assert.That(file, Is.Not.Null); + Assert.That(File.Exists(file), Is.True); + } + + [Test] + public void NetCoreAppResolvesCompatibleSharedRuntimeAssembly() + { + var reference = AssemblyNameReference.Parse( + "System.Text.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"); + var resolver = new UniversalAssemblyResolver(null, false, ".NETCoreApp,Version=v3.1"); + + var file = resolver.FindAssemblyFile(reference); + + Assert.That(file, Is.Not.Null); + Assert.That(File.Exists(file), Is.True); + Assert.That(AssemblyName.GetAssemblyName(file!).Version, Is.GreaterThanOrEqualTo(reference.Version)); + } + + [Test] + public void LaterRuntimeAssemblyResolvesWithoutBecomingImplicitProjectReference() + { + var reference = AssemblyNameReference.Parse( + "System.Net.ServerSentEvents, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + var resolver = new UniversalAssemblyResolver(null, false, ".NETCoreApp,Version=v8.0"); + + Assert.That(resolver.FindAssemblyFile(reference), Is.Not.Null); + Assert.That(resolver.IsSharedAssembly(reference, out _), Is.False); + } + + [TestCase(TargetFrameworkIdentifier.NETStandard, ".NETStandard,Version=v2.0", PlatformID.Win32NT, false)] + [TestCase(TargetFrameworkIdentifier.NET, ".NETFramework,Version=v4.7.2", PlatformID.Win32NT, false)] + [TestCase(TargetFrameworkIdentifier.NETCoreApp, ".NETCoreApp,Version=v3.1", PlatformID.Win32NT, true)] + [TestCase(TargetFrameworkIdentifier.NET, ".NETCoreApp,Version=v10.0", PlatformID.Win32NT, true)] + [TestCase(TargetFrameworkIdentifier.NETStandard, ".NETStandard,Version=v2.0", PlatformID.Unix, true)] + public void VerifyHostRuntimeFallback(TargetFrameworkIdentifier identifier, string targetFramework, + PlatformID platform, bool expected) + { + Assert.That(UniversalAssemblyResolver.ShouldUseHostRuntimeFallback(identifier, targetFramework, platform), + Is.EqualTo(expected)); + } } } diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs index 4d0d25c8e7..8ae06565e7 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs @@ -28,6 +28,9 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + using NUnit.Framework; namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler; @@ -134,6 +137,75 @@ public void OneFailingResourceDoesNotDropTheOthers() } } + [Test] + public void HiddenReferencedTypesDoNotCreateEmptyProjectFiles() + { + string assemblyPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".dll"); + try + { + CompileCollectionExpressionAssembly(assemblyPath); + TestFriendlyProjectDecompiler decompiler = new(new UniversalAssemblyResolver(assemblyPath, false, null)); + decompiler.Settings.CollectionExpressions = true; + + using PEFile module = new(assemblyPath); + decompiler.DecompileProject(module, Path.GetTempPath(), new StringWriter()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(decompiler.Files.Keys.Select(Path.GetFileName), + Has.None.StartsWith("--z__ReadOnly")); + string source = decompiler.Files.Single(file => Path.GetFileName(file.Key) == "CollectionSource.cs") + .Value.ToString(); + Assert.That(source, Does.Contain("Consume([1]);")); + Assert.That(source, Does.Contain("Consume([1, 2, 3]);")); + Assert.That(source, Does.Match(@"Consume\(\[\.\. \w+\]\);")); + } + } + finally + { + File.Delete(assemblyPath); + } + } + + static void CompileCollectionExpressionAssembly(string assemblyPath) + { + const string source = """ + using System.Collections.Generic; + + public static class CollectionSource + { + public static void Call(int[] values) + { + Consume([1]); + Consume([1, 2, 3]); + Consume([0, .. values, 4]); + } + + private static void Consume(IReadOnlyList values) + { + } + } + """; + string runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var compilation = CSharpCompilation.Create( + "CollectionExpressionProject", + new[] { + CSharpSyntaxTree.ParseText(source, + new CSharpParseOptions(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp12)) + }, + new[] { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(Path.Combine(runtimeDirectory, "System.Runtime.dll")) + }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + + using FileStream output = File.Create(assemblyPath); + var result = compilation.Emit(output); + Assert.That(result.Success, Is.True, + string.Join(Environment.NewLine, result.Diagnostics.Select(diagnostic => diagnostic.ToString()))); + } + sealed class ThrowingAstTransform(string typeName) : IAstTransform { public const string Failure = "Simulated AST transform failure"; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CachedReadOnlySpanFromLazyCache.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CachedReadOnlySpanFromLazyCache.cs index d29d6c3791..eb7d928952 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CachedReadOnlySpanFromLazyCache.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CachedReadOnlySpanFromLazyCache.cs @@ -4,10 +4,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty { public static class CachedReadOnlySpanFromLazyCache { - public static ReadOnlySpan NewLine { - get { - return new ReadOnlySpan(new char[2] { '\r', '\n' }); - } - } + public static ReadOnlySpan NewLine => new ReadOnlySpan(new char[2] { '\r', '\n' }); } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.cs new file mode 100644 index 0000000000..e1dc5d088b --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ +#if !EXPECTED_OUTPUT + public struct MissingMemory + { + } +#endif + + [CompilerGenerated] + public sealed class CompilerGeneratedAutoProperty + { + public string Name { get; } + + public CompilerGeneratedAutoProperty(string name) + { + Name = name; + } + } + + public class UnresolvedGenericAutoProperty + { + public MissingMemory Data { get; set; } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.il new file mode 100644 index 0000000000..276f52dc41 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompilerGeneratedAutoProperty.il @@ -0,0 +1,79 @@ +.assembly extern System.Runtime +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89) + .ver 4:0:0:0 +} + +.assembly CompilerGeneratedAutoProperty +{ + .hash algorithm 0x00008004 + .ver 0:0:0:0 +} + +.assembly extern MissingGenericAssembly +{ + .ver 1:0:0:0 +} + +.class public auto ansi sealed beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.CompilerGeneratedAutoProperty + extends [System.Runtime]System.Object +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00) + .field private initonly string 'k__BackingField' + + .method public hidebysig specialname instance string get_Name() cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldfld string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.CompilerGeneratedAutoProperty::'k__BackingField' + IL_0006: ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor(string name) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ldarg.0 + IL_0007: ldarg.1 + IL_0008: stfld string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.CompilerGeneratedAutoProperty::'k__BackingField' + IL_000d: ret + } + + .property instance string Name() + { + .get instance string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.CompilerGeneratedAutoProperty::get_Name() + } +} + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.UnresolvedGenericAutoProperty`1 + extends [System.Runtime]System.Object +{ + .field private valuetype [MissingGenericAssembly]MissingMemory`1 'k__BackingField' + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00) + + .method public hidebysig specialname instance valuetype [MissingGenericAssembly]MissingMemory`1 get_Data() cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldfld valuetype [MissingGenericAssembly]MissingMemory`1 ICSharpCode.Decompiler.Tests.TestCases.ILPretty.UnresolvedGenericAutoProperty`1::'k__BackingField' + IL_0006: ret + } + + .method public hidebysig specialname instance void set_Data(valuetype [MissingGenericAssembly]MissingMemory`1 'value') cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld valuetype [MissingGenericAssembly]MissingMemory`1 ICSharpCode.Decompiler.Tests.TestCases.ILPretty.UnresolvedGenericAutoProperty`1::'k__BackingField' + IL_0007: ret + } + + .property instance valuetype [MissingGenericAssembly]MissingMemory`1 Data() + { + .get instance valuetype [MissingGenericAssembly]MissingMemory`1 ICSharpCode.Decompiler.Tests.TestCases.ILPretty.UnresolvedGenericAutoProperty`1::get_Data() + .set instance void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.UnresolvedGenericAutoProperty`1::set_Data(valuetype [MissingGenericAssembly]MissingMemory`1) + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs index 4ac1684c30..8b073936a8 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs @@ -66,6 +66,13 @@ public void TestRefTypeNewobj() MyClass value = new MyClass(); Console.WriteLine(value); } + + public void TestUnresolvedStructMemberCalls() + { + MyEnumerator val = default; + val.MoveNext(); + ((IDisposable)val/*cast due to constrained. prefix*/).Dispose(); + } } public class Issue3729_DerivedFromUnknown : MissingBase { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il index f468655e6e..8ed0374995 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il @@ -177,6 +177,23 @@ IL_000c: ret } + .method public hidebysig + instance void TestUnresolvedStructMemberCalls () cil managed + { + .maxstack 1 + .locals init ( + [0] valuetype [Library1]Library1.MyEnumerator + ) + + IL_0000: ldloca.s 0 + IL_0002: call instance bool [Library1]Library1.MyEnumerator::MoveNext() + IL_0007: pop + IL_0008: ldloca.s 0 + IL_000a: constrained. [Library1]Library1.MyEnumerator + IL_0010: callvirt instance void [System.Runtime]System.IDisposable::Dispose() + IL_0015: ret + } + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.cs new file mode 100644 index 0000000000..aa7abd40d0 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.cs @@ -0,0 +1,44 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public class LateBaseConstructorCall + { + private static void Initialize() + { + } + + public LateBaseConstructorCall() + { + Initialize(); + } + } + + public class SpilledArgumentSource + { + public string Content; + public string Refusal; + public object Output; + public string Function; + } + + public class SpilledConstructorInitializer + { + private SpilledConstructorInitializer(SpilledRole role, string content, in SpilledPatch patch, string refusal, string participantName, object output, object tools, string function) + { + } + + public SpilledConstructorInitializer(SpilledArgumentSource source) + : this(content: source?.Content, patch: default, refusal: source?.Refusal, participantName: null, function: source?.Function, role: SpilledRole.Assistant, output: (source?.Output != null) ? new object() : null, tools: null) + { + } + } + + public struct SpilledPatch + { + public int Value; + } + + public enum SpilledRole + { + Assistant = 2 + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.il new file mode 100644 index 0000000000..2c026cbbb9 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/LateBaseConstructorCall.il @@ -0,0 +1,135 @@ +.assembly extern System.Runtime +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89) + .ver 4:0:0:0 +} + +.assembly LateBaseConstructorCall +{ + .hash algorithm 0x00008004 + .ver 0:0:0:0 +} + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.LateBaseConstructorCall + extends [System.Runtime]System.Object +{ + .method private hidebysig static void Initialize() cil managed + { + .maxstack 8 + IL_0000: ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + IL_0000: call void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.LateBaseConstructorCall::Initialize() + IL_0005: ldarg.0 + IL_0006: call instance void [System.Runtime]System.Object::.ctor() + IL_000b: ret + } +} + +.class public auto ansi sealed ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledRole + extends [System.Runtime]System.Enum +{ + .field public specialname rtspecialname int32 value__ + .field public static literal valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledRole Assistant = int32(2) +} + +.class public sequential ansi sealed beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledPatch + extends [System.Runtime]System.ValueType +{ + .field public int32 Value +} + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource + extends [System.Runtime]System.Object +{ + .field public string Content + .field public string Refusal + .field public object Output + .field public string Function +} + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledConstructorInitializer + extends [System.Runtime]System.Object +{ + .method private hidebysig specialname rtspecialname instance void .ctor( + valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledRole role, + string content, + [in] valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledPatch& patch, + string refusal, + string participantName, + object output, + object tools, + string function + ) cil managed + { + .param [3] + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (01 00 00 00) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor( + class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource source + ) cil managed + { + .maxstack 9 + .locals init ( + [0] string function, + [1] valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledPatch patch + ) + + IL_0000: ldarg.0 + IL_0001: ldc.i4.2 + IL_0002: ldarg.1 + IL_0003: brtrue.s IL_0008 + IL_0005: ldnull + IL_0006: br.s IL_000e + IL_0008: ldarg.1 + IL_0009: ldfld string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource::Content + IL_000e: ldloca.s patch + IL_0010: dup + IL_0011: initobj ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledPatch + IL_0017: ldarg.1 + IL_0018: brtrue.s IL_001d + IL_001a: ldnull + IL_001b: br.s IL_0023 + IL_001d: ldarg.1 + IL_001e: ldfld string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource::Refusal + IL_0023: ldnull + IL_0024: ldarg.1 + IL_0025: brtrue.s IL_002a + IL_0027: ldnull + IL_0028: br.s IL_0030 + IL_002a: ldarg.1 + IL_002b: ldfld string ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource::Function + IL_0030: stloc.0 + IL_0031: ldarg.1 + IL_0032: brtrue.s IL_0037 + IL_0034: ldnull + IL_0035: br.s IL_003d + IL_0037: ldarg.1 + IL_0038: ldfld object ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledArgumentSource::Output + IL_003d: brtrue.s IL_0042 + IL_003f: ldnull + IL_0040: br.s IL_0047 + IL_0042: newobj instance void [System.Runtime]System.Object::.ctor() + IL_0047: ldnull + IL_0048: ldloc.0 + IL_0049: call instance void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledConstructorInitializer::.ctor( + valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledRole, + string, + valuetype ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpilledPatch&, + string, + string, + object, + object, + string + ) + IL_004e: ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.cs new file mode 100644 index 0000000000..794871ecef --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.cs @@ -0,0 +1,10 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public class MissingBaseConstructor : MissingBase + { + public MissingBaseConstructor(MissingRole role, string content) + : base(role, content) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.il new file mode 100644 index 0000000000..0c01dbc249 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/MissingBaseConstructor.il @@ -0,0 +1,36 @@ +.assembly extern System.Runtime +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89) + .ver 4:0:0:0 +} + +.assembly extern MissingBaseAssembly +{ + .ver 1:0:0:0 +} + +.assembly MissingBaseConstructor +{ + .hash algorithm 0x00008004 + .ver 0:0:0:0 +} + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.MissingBaseConstructor + extends [MissingBaseAssembly]MissingBase +{ + .method public hidebysig specialname rtspecialname instance void .ctor( + valuetype [MissingBaseAssembly]MissingRole role, + string content + ) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldarg.2 + IL_0003: call instance void [MissingBaseAssembly]MissingBase::.ctor( + valuetype [MissingBaseAssembly]MissingRole, + string + ) + IL_0008: ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/GlobalNamespaceDocument.cs b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/GlobalNamespaceDocument.cs new file mode 100644 index 0000000000..9b4eba89e8 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/GlobalNamespaceDocument.cs @@ -0,0 +1,4 @@ +public class GlobalNamespaceDocument +{ + public int Value => 1; +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CachedReadOnlySpanInitialization.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CachedReadOnlySpanInitialization.cs index 18839d4607..dd0a8d9778 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CachedReadOnlySpanInitialization.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CachedReadOnlySpanInitialization.cs @@ -8,7 +8,9 @@ public static class CachedReadOnlySpanInitialization // compiler-generated lazy cache in for a ReadOnlySpan // created from a multi-byte array literal. The CachedReadOnlySpanInitialization transform // collapses that cache back to the explicit ReadOnlySpan constructor. -#if NET70 +#if EXPECTED_OUTPUT && CS120 + public static ReadOnlySpan NewLine => ['\r', '\n']; +#elif NET70 public static ReadOnlySpan NewLine => new char[2] { '\r', '\n' }; #else public static ReadOnlySpan NewLine => new ReadOnlySpan(new char[2] { '\r', '\n' }); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CollectionExpressions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CollectionExpressions.cs new file mode 100644 index 0000000000..c7bd511059 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CollectionExpressions.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + [CollectionBuilder(typeof(BuilderCollectionFactory), "Create")] + public sealed class BuilderCollection : IEnumerable, IEnumerable + { + private readonly int[] items; + + public BuilderCollection(int[] items) + { + this.items = items; + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)items).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + } + + public static class BuilderCollectionFactory + { + public static BuilderCollection Create(ReadOnlySpan items) + { + return new BuilderCollection(items.ToArray()); + } + } + + public static class CollectionExpressions + { + public static int[] EmptyArray() + { + return []; + } + + public static int[] ArrayElements() + { + return [1, 2, 3]; + } + + public static int[] ArraySpread(IEnumerable items) + { + return [0, .. items, 4]; + } + + public static List EmptyList() + { + return []; + } + + public static List ListElements() + { + return [1, 2, 3]; + } + + public static List ListSpread(IEnumerable first, int[] second) + { + return [0, .. first, 1, .. second, 2]; + } + + public static IList InterfaceElements() + { + return [1, 2, 3]; + } + + public static IEnumerable EnumerableElements() + { + return [1, 2, 3]; + } + + public static int SpanElements() + { +#if EXPECTED_OUTPUT +#if ROSLYN5 + Span inlineArray = [1, 2, 3]; + return inlineArray[0]; +#else + Span obj = [1, 2, 3]; + return obj[0]; +#endif +#else + Span span = [1, 2, 3]; + return span[0]; +#endif + } + + public static int ReadOnlySpanElements() + { +#if EXPECTED_OUTPUT + return ((ReadOnlySpan)[1, 2, 3])[0]; +#else + ReadOnlySpan span = [1, 2, 3]; + return span[0]; +#endif + } + + public static BuilderCollection BuilderElements() + { + return [1, 2, 3]; + } + + public static BuilderCollection BuilderSpread(IEnumerable items) + { + return [0, .. items, 4]; + } + + public static int[,] MultiDimensionalArray() + { + return new int[2, 3] { + { 0, 1, 2 }, + { 3, 4, 5 } + }; + } + + public static List RecursiveList() + { + List list = []; + list.Add(list); + return list; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs index 0be38a28cf..1d8667fdba 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs @@ -201,6 +201,54 @@ public MethodCallInCtorInit(string s) } } +#if CS120 + public class SpreadConstructorBase + { + protected SpreadConstructorBase(IList items) + { + } + } + + public class SpreadConstructorInitializers : SpreadConstructorBase + { + public SpreadConstructorInitializers() + : base([]) + { + } + + public SpreadConstructorInitializers(IEnumerable items) + : base([.. items]) + { + } + + public SpreadConstructorInitializers(IReadOnlyList items, bool unused) + : base([.. items]) + { + } + + public SpreadConstructorInitializers(params int[] items) + : base([.. items]) + { + } + + public SpreadConstructorInitializers(IEnumerable items, int unused) + : base([1, 2, 3]) + { + } + + public SpreadConstructorInitializers(IEnumerable items, string unused) + : base([1, .. items, 2]) + { + } + + public SpreadConstructorInitializers(IEnumerable first, IEnumerable second) + : base([.. first, .. second]) + { + } + } + +#endif + public struct SimpleStruct { public int Field1; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FieldKeyword.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FieldKeyword.cs index 6938e60698..7ea3a64312 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FieldKeyword.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FieldKeyword.cs @@ -155,6 +155,35 @@ public static void Reset() } } + public record RecordWithAutoProperties + { + public int Value { get; init; } + + public string Text { get; init; } = ""; + + public RecordWithAutoProperties(int value) + { + Value = value; + } + } + + public abstract record FieldBackedRecordBase + { + public int Id { get; init; } + } + + public record RecordWithFieldBackedProperty : FieldBackedRecordBase + { + public string Value { + get { + return field ?? string.Empty; + } + init { + field = value ?? string.Empty; + } + } + } + // 0.00m and -0.0 compare equal to their defaults but are observably different, so // neither initializer may be dropped as a redundant default. public struct PreciseDefaults diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs index 75f65536fa..6dd3680839 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -45,6 +45,16 @@ public record Fields public string S = "abc"; } + public record PrivateFieldsBeforeProperties + { + private readonly int hiddenNumber = 42; + private readonly string hiddenText = "hidden"; + + public int A { get; init; } + public string B { get; init; } + public int HiddenValue => hiddenNumber + hiddenText.Length; + } + public record Interface(int B) : IRecord; public interface IRecord @@ -73,6 +83,18 @@ public record PrimaryCtorWithProperty(int A, string B) public string D { get; } = A + B; } + public record PrimaryCtorWithReorderedAssignments(int first, int second, int Third, int Fourth) + { + private int first { get; init; } = first; + private int second { get; init; } = second; + } + + public record WithIndexer + { + public int A { get; init; } + public int this[int index] => A + index; + } + public record Properties { public int A { get; set; } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs index dde61d8b2d..6ae40a6f36 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs @@ -12,13 +12,8 @@ // A VB anonymous type. Its properties are settable and only those declared 'Key' // take part in Equals and GetHashCode, so it cannot be written as a C# anonymous // type and is declared here instead. -#if LEGACY_VBC && OPT [DebuggerDisplay("Value={Value}, Name={Name}")] [CompilerGenerated] -#else -[CompilerGenerated] -[DebuggerDisplay("Value={Value}, Name={Name}")] -#endif internal sealed class VB_AnonymousType_0 { #if !OPT && !LEGACY_VBC diff --git a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs index 3cd9f3b09b..5e92026352 100644 --- a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs @@ -129,7 +129,8 @@ public async Task Select([ValueSource(nameof(defaultOptions))] CompilerOptions o public async Task VBAnonymousTypes([ValueSource(nameof(defaultOptions))] CompilerOptions options) { IgnoreIfVbRuntimeSubstituted(options); - await Run(options: options | CompilerOptions.Library); + await Run(options: options | CompilerOptions.Library, + settings: new DecompilerSettings { FileScopedNamespaces = false, SortCustomAttributes = true }); } [Test] @@ -193,7 +194,9 @@ async Task Run([CallerMemberName] string testName = null, CompilerOptions option } var executable = await Tester.CompileVB(vbFile, options | CompilerOptions.ReferenceVisualBasic, exeFile).ConfigureAwait(false); - var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings ?? new DecompilerSettings { FileScopedNamespaces = false }).ConfigureAwait(false); + settings ??= new DecompilerSettings { FileScopedNamespaces = false }; + settings.CollectionExpressions = false; + var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings).ConfigureAwait(false); CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).ToArray()); Tester.RepeatOnIOError(() => File.Delete(decompiled)); diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index ad511a4930..e5c916b288 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -244,6 +244,7 @@ public static List GetAstTransforms() new AddCheckedBlocks(), new DeclareVariables(), // should run after most transforms that modify statements new TransformFieldAndConstructorInitializers(), // must run after DeclareVariables + new IntroduceCollectionExpressions(), new PrettifyAssignments(), // must run after DeclareVariables new IntroduceUsingDeclarations(), new IntroduceExtensionMethods(), // must run after IntroduceUsingDeclarations @@ -418,6 +419,14 @@ public static bool MemberIsHidden(MetadataFile? module, EntityHandle member, Dec var typeHandle = (TypeDefinitionHandle)member; var type = metadata.GetTypeDefinition(typeHandle); name = metadata.GetString(type.Name); + if (settings.CollectionExpressions + && (name.StartsWith("<>y__InlineArray", StringComparison.Ordinal) + || name.StartsWith("<>z__ReadOnlyArray", StringComparison.Ordinal) + || name.StartsWith("<>z__ReadOnlyList", StringComparison.Ordinal) + || name.StartsWith("<>z__ReadOnlySingleElementList", StringComparison.Ordinal))) + { + return true; + } if (!type.GetDeclaringType().IsNil) { if (settings.LocalFunctions && LocalFunctionDecompiler.IsLocalFunctionDisplayClass(module, typeHandle)) diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index a59e50abf8..9d1bafaaad 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2879,7 +2879,17 @@ internal TranslatedExpression TranslateTarget(ILInstruction? target, bool nonVir else { IType targetTypeHint = constrainedTo ?? memberDeclaringType; - if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo) == StackType.Ref) + if (target is Conv { + Kind: ConversionKind.Invalid, + InputType: StackType.Ref, + TargetType: IL.PrimitiveType.Unknown + } conv && targetTypeHint.Kind == TypeKind.Unknown) + { + target = conv.Argument; + } + StackType expectedThisPointerType = CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo); + if (expectedThisPointerType == StackType.Ref + || (expectedThisPointerType == StackType.Unknown && target.ResultType == StackType.Ref)) { if (target.ResultType == StackType.Ref) { @@ -2891,13 +2901,17 @@ internal TranslatedExpression TranslateTarget(ILInstruction? target, bool nonVir } } var translatedTarget = Translate(target, targetTypeHint); - if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo) == StackType.Ref) + if (expectedThisPointerType == StackType.Ref + || (expectedThisPointerType == StackType.Unknown && target.ResultType == StackType.Ref)) { // When accessing members on value types, ensure we use a reference of the correct type, // and not a pointer or a reference to a different type (issue #1333) - if (!(translatedTarget.Type is ByReferenceType brt && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(brt.ElementType, constrainedTo ?? memberDeclaringType))) + IType expectedTargetType = constrainedTo ?? memberDeclaringType; + if (!(translatedTarget.Type is ByReferenceType brt + && (NormalizeTypeVisitor.TypeErasure.EquivalentTypes(brt.ElementType, expectedTargetType) + || (expectedTargetType.Kind == TypeKind.Unknown && brt.ElementType.ReflectionName == expectedTargetType.ReflectionName)))) { - translatedTarget = translatedTarget.ConvertTo(new ByReferenceType(constrainedTo ?? memberDeclaringType), this); + translatedTarget = translatedTarget.ConvertTo(new ByReferenceType(expectedTargetType), this); } } if (translatedTarget.Expression is DirectionExpression) diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 0cd5ac7610..f6a880595c 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -658,6 +658,22 @@ public virtual void VisitArrayInitializerExpression(ArrayInitializerExpression a EndNode(arrayInitializerExpression); } + public virtual void VisitCollectionExpression(CollectionExpression collectionExpression) + { + StartNode(collectionExpression); + WriteCommaSeparatedListInBrackets(collectionExpression.Elements); + EndNode(collectionExpression); + } + + public virtual void VisitSpreadElement(SpreadElement spreadElement) + { + StartNode(spreadElement); + WriteToken(BinaryOperatorExpression.RangeToken); + Space(); + spreadElement.Expression.AcceptVisitor(this); + EndNode(spreadElement); + } + protected bool CanBeConfusedWithObjectInitializer(Expression expr) { // "int a; new List { a = 1 };" is an object initalizers and invalid, but diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs index cdc1ec2dbe..82d5ea9df3 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs @@ -59,4 +59,9 @@ public interface IProjectInfoProvider /// string StrongNameKeyFile { get; } } + + internal interface INullableProjectInfoProvider + { + bool NullableReferenceTypes { get; } + } } diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index e7d75c568d..8c49583477 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -219,6 +219,8 @@ static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project) xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); xml.WriteElementString("AllowUnsafeBlocks", TrueString); xml.WriteElementString("CheckForOverflowUnderflow", project.CheckForOverflowUnderflow ? TrueString : FalseString); + if (project is INullableProjectInfoProvider { NullableReferenceTypes: true }) + xml.WriteElementString("Nullable", "enable"); if (project.StrongNameKeyFile != null) { diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 9264e109a1..d8e65ff19f 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -48,7 +48,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// /// Decompiles an assembly into a visual studio project file. /// - public class WholeProjectDecompiler : IProjectInfoProvider + public class WholeProjectDecompiler : IProjectInfoProvider, INullableProjectInfoProvider { const int maxSegmentLength = 255; @@ -87,6 +87,8 @@ void ValidateLanguageVersion(LanguageVersion version) bool IProjectInfoProvider.CheckForOverflowUnderflow => Settings.CheckForOverflowUnderflow; + bool INullableProjectInfoProvider.NullableReferenceTypes => Settings.NullableReferenceTypes; + public IAssemblyResolver AssemblyResolver { get; } public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 373c14f540..db1cb24510 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -28,6 +28,7 @@ using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; @@ -57,8 +58,9 @@ public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef this.settings = settings; this.cancellationToken = cancellationToken; this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class); - this.isStruct = baseClass?.IsKnownType(KnownTypeCode.ValueType) ?? false; - this.isInheritedRecord = !isStruct && !(baseClass?.IsKnownType(KnownTypeCode.Object) ?? false); + this.isStruct = recordTypeDef.Kind == TypeKind.Struct; + var baseClassDefinition = baseClass?.GetDefinition(); + this.isInheritedRecord = !isStruct && (baseClassDefinition?.IsRecord ?? false); this.isSealed = recordTypeDef.IsSealed; DetectAutomaticProperties(); this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); @@ -72,7 +74,8 @@ void DetectAutomaticProperties() { cancellationToken.ThrowIfCancellationRequested(); var p = (IProperty)property.Specialize(subst); - if (IsAutoProperty(p, out var field)) + if (IsAutoProperty(p, out var field) + || (settings.FieldKeyword && PatternStatementTransform.TryGetBackingField(p, out field))) { backingFieldToAutoProperty.Add(field, p); autoPropertyToBackingField.Add(p, field); @@ -293,8 +296,6 @@ bool IsPrimaryConstructor(Block body, IMethod method, IMethod unspecializedMetho if (body.Instructions.Count < addonInst) return false; - int parameterIndex = 0; - for (int i = 0; i < body.Instructions.Count - addonInst; i++) { if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst)) @@ -306,16 +307,14 @@ bool IsPrimaryConstructor(Block body, IMethod method, IMethod unspecializedMetho continue; if (valueInst.MatchLdLoc(out var v)) { - if (!ValidateParameter(v, parameterIndex)) + if (!ValidateParameter(v)) return false; - parameterIndex = v.Index!.Value; } else if (valueInst.MatchLdObj(out valueInst, out _) && valueInst.MatchLdLoc(out v)) { - if (!ValidateParameter(v, parameterIndex)) + if (!ValidateParameter(v)) return false; - parameterIndex = v.Index!.Value; - if (method.Parameters[parameterIndex].ReferenceKind is ReferenceKind.None) + if (method.Parameters[v.Index!.Value].ReferenceKind is ReferenceKind.None) { return false; } @@ -324,7 +323,7 @@ bool IsPrimaryConstructor(Block body, IMethod method, IMethod unspecializedMetho { continue; } - IParameter parameter = unspecializedMethod.Parameters[parameterIndex]; + IParameter parameter = unspecializedMethod.Parameters[v.Index!.Value]; if (primaryCtorParameterToAutoProperty.ContainsKey(parameter)) { continue; @@ -332,7 +331,8 @@ bool IsPrimaryConstructor(Block body, IMethod method, IMethod unspecializedMetho if (recordTypeDef.Kind != TypeKind.Struct) { - if (!(property.CanSet && property.Setter.IsInitOnly)) + if (property.Accessibility != Accessibility.Public + || !(property.CanSet && property.Setter.IsInitOnly)) { continue; } @@ -343,17 +343,12 @@ bool IsPrimaryConstructor(Block body, IMethod method, IMethod unspecializedMetho var returnInst = body.Instructions.LastOrDefault(); return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop(); - bool ValidateParameter(ILVariable v, int expectedMinimumIndex) + bool ValidateParameter(ILVariable v) { if (v.Kind != VariableKind.Parameter) return false; Debug.Assert(v.Index.HasValue); - if (v.Index < 0 || v.Index >= unspecializedMethod.Parameters.Count) - return false; - var parameter = unspecializedMethod.Parameters[v.Index.Value]; - if (primaryCtorParameterToAutoProperty.ContainsKey(parameter)) - return true; - return v.Index >= expectedMinimumIndex; + return v.Index >= 0 && v.Index < unspecializedMethod.Parameters.Count; } } @@ -377,16 +372,36 @@ bool ValidateParameter(ILVariable v, int expectedMinimumIndex) static List DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary backingFieldToAutoProperty) { - // For records, the order of members is important: - // Equals/GetHashCode/PrintMembers must agree on an order of fields+properties. - // The IL metadata has the order of fields and the order of properties, but we - // need to detect the correct interleaving. - // We could try to detect this from the PrintMembers body, but let's initially - // restrict ourselves to the common case where the record only uses properties. var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); - return recordTypeDef.Properties.Select(p => p.Specialize(subst)).Concat( - recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst)).Where(f => !backingFieldToAutoProperty.ContainsKey(f)) - ).ToList(); + var properties = recordTypeDef.Properties.Select(p => (IProperty)p.Specialize(subst)).ToList(); + var result = new List(); + foreach (var field in recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst))) + { + result.Add(backingFieldToAutoProperty.TryGetValue(field, out var property) ? property : field); + } + + for (int i = 0; i < properties.Count; i++) + { + var property = properties[i]; + if (result.Contains(property)) + continue; + if (recordTypeDef.Kind == TypeKind.Class && property.Name == "EqualityContract") + { + result.Insert(0, property); + continue; + } + + var nextPropertyWithStorage = properties.Skip(i + 1).FirstOrDefault(result.Contains); + if (nextPropertyWithStorage != null) + { + result.Insert(result.IndexOf(nextPropertyWithStorage), property); + } + else + { + result.Insert(result.FindLastIndex(member => member is IProperty) + 1, property); + } + } + return result; } /// @@ -523,6 +538,7 @@ private bool IsAllowedAttribute(IAttribute attribute) switch (attribute.AttributeType.ReflectionName) { case "System.Runtime.CompilerServices.CompilerGeneratedAttribute": + case "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute": return true; default: return false; @@ -553,7 +569,7 @@ leave IL_0000 (nop) // First instruction is the base constructor call if (!(body.Instructions[pos] is Call { Method: { IsConstructor: true } } baseCtorCall)) return false; - if (!object.Equals(baseCtorCall.Method.DeclaringType, baseClass)) + if (baseClass == null || !NormalizeTypeVisitor.TypeErasure.EquivalentTypes(baseCtorCall.Method.DeclaringType, baseClass)) return false; if (baseCtorCall.Arguments.Count != (isInheritedRecord ? 2 : 1)) return false; @@ -565,7 +581,7 @@ leave IL_0000 (nop) return false; } pos++; - // Then all the fields are copied over + var expectedFields = new HashSet(); foreach (var member in orderedMembers) { if (member.IsStatic) @@ -575,24 +591,29 @@ leave IL_0000 (nop) if (!autoPropertyToBackingField.TryGetValue((IProperty)member, out field!)) continue; } + expectedFields.Add((IField)field.MemberDefinition); + } + + while (pos < body.Instructions.Count && body.Instructions[pos] is not Leave) + { if (pos >= body.Instructions.Count) return false; if (!body.Instructions[pos].MatchStFld(out var lhsTarget, out var lhsField, out var valueInst)) return false; if (!lhsTarget.MatchLdThis()) return false; - if (!lhsField.Equals(field)) + if (!expectedFields.Remove((IField)lhsField.MemberDefinition)) return false; if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField)) return false; if (!rhsTarget.MatchLdLoc(other)) return false; - if (!rhsField.Equals(field)) + if (!rhsField.MemberDefinition.Equals(lhsField.MemberDefinition)) return false; pos++; } - return body.Instructions[pos] is Leave; + return expectedFields.Count == 0 && pos < body.Instructions.Count && body.Instructions[pos] is Leave; } private bool IsGeneratedEqualityContract(IProperty property) @@ -795,6 +816,10 @@ bool IsPrintedMember(IMember member) { return false; // override is not printed (again), the virtual base property was already printed } + if (member is IProperty { Parameters.Count: > 0 }) + { + return false; + } return true; } @@ -1150,7 +1175,7 @@ bool ProcessIndividualHashCode(ILInstruction inst) if (foundBaseClassHash || hashedMembers.Count > 0) return false; // must be first foundBaseClassHash = true; - return baseHashCodeCall.Method.DeclaringType.Equals(baseClass); + return baseClass != null && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(baseHashCodeCall.Method.DeclaringType, baseClass); } // callvirt GetHashCode(call get_Default(), callvirt get_EqualityContract(ldloc this)) // callvirt GetHashCode(call get_Default(), ldfld k__BackingField(ldloc this))) diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs index 0e07ca7bac..4af7fec85f 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -1241,7 +1241,9 @@ IReadOnlyList FindTypesInBounds(IReadOnlyList lowerBounds, IReadOn else { // Find candidates by looking at all classes in the project: - candidateTypeDefinitions = compilation.GetAllTypeDefinitions().ToList(); + candidateTypeDefinitions = compilation.GetAllTypeDefinitions() + .Where(type => !type.IsCompilerGenerated()) + .ToList(); } // Now filter out candidates that violate the upper bounds: diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index 692fb3a16f..2181002133 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -659,6 +659,16 @@ public virtual void VisitArrayInitializerExpression(ArrayInitializerExpression a VisitChildren(arrayInitializerExpression); } + public virtual void VisitCollectionExpression(CollectionExpression collectionExpression) + { + VisitChildren(collectionExpression); + } + + public virtual void VisitSpreadElement(SpreadElement spreadElement) + { + VisitChildren(spreadElement); + } + public virtual void VisitArraySpecifier(ArraySpecifier arraySpecifier) { VisitChildren(arraySpecifier); @@ -1334,6 +1344,16 @@ public virtual T VisitArrayInitializerExpression(ArrayInitializerExpression arra return VisitChildren(arrayInitializerExpression); } + public virtual T VisitCollectionExpression(CollectionExpression collectionExpression) + { + return VisitChildren(collectionExpression); + } + + public virtual T VisitSpreadElement(SpreadElement spreadElement) + { + return VisitChildren(spreadElement); + } + public virtual T VisitArraySpecifier(ArraySpecifier arraySpecifier) { return VisitChildren(arraySpecifier); @@ -2009,6 +2029,16 @@ public virtual S VisitArrayInitializerExpression(ArrayInitializerExpression arra return VisitChildren(arrayInitializerExpression, data); } + public virtual S VisitCollectionExpression(CollectionExpression collectionExpression, T data) + { + return VisitChildren(collectionExpression, data); + } + + public virtual S VisitSpreadElement(SpreadElement spreadElement, T data) + { + return VisitChildren(spreadElement, data); + } + public virtual S VisitArraySpecifier(ArraySpecifier arraySpecifier, T data) { return VisitChildren(arraySpecifier, data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/CollectionExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/CollectionExpression.cs new file mode 100644 index 0000000000..fe27bcc1e3 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/CollectionExpression.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2026 sonyps5201314 +// +// 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. + +#nullable enable + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + /// + /// 集合表达式,例如 [1, 2, .. items]。 + /// + [DecompilerAstNode] + public sealed partial class CollectionExpression : Expression + { + [Slot("CollectionElement")] + public partial AstNodeCollection Elements { get; } + } + + /// + /// 集合表达式中的展开元素,例如 .. items。 + /// + [DecompilerAstNode] + public sealed partial class SpreadElement : Expression + { + [Slot("Expression")] + public partial Expression Expression { get; set; } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceCollectionExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceCollectionExpressions.cs new file mode 100644 index 0000000000..58022be27c --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceCollectionExpressions.cs @@ -0,0 +1,483 @@ +// Copyright (c) 2026 sonyps5201314 +// +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; + +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.Semantics; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.CSharp.Transforms +{ + public sealed class IntroduceCollectionExpressions : DepthFirstAstVisitor, IAstTransform + { + TransformContext context = null!; + + public void Run(AstNode node, TransformContext context) + { + this.context = context; + if (context.Settings.CollectionExpressions) + node.AcceptVisitor(this); + } + + public override void VisitBlockStatement(BlockStatement blockStatement) + { + bool changed; + do + { + changed = false; + foreach (var statement in blockStatement.Statements.ToArray()) + { + if (TryTransformListConstruction(blockStatement, statement) + || TryTransformInlineArrayConstruction(blockStatement, statement)) + { + changed = true; + break; + } + } + } + while (changed); + base.VisitBlockStatement(blockStatement); + } + + public override void VisitCastExpression(CastExpression castExpression) + { + base.VisitCastExpression(castExpression); + var targetType = castExpression.Type.GetResolveResult().Type; + if (!TryConvertExpression(castExpression.Expression, targetType, out var collection)) + return; + context.Step("Use collection expression", castExpression.Expression); + castExpression.Expression.ReplaceWith(collection); + context.EndStep(collection); + } + + public override void VisitObjectCreateExpression(ObjectCreateExpression objectCreateExpression) + { + base.VisitObjectCreateExpression(objectCreateExpression); + var objectType = objectCreateExpression.GetResolveResult().Type; + if (!IsReadOnlyCollectionHelper(objectType) + || objectType.DirectBaseTypes.FirstOrDefault(type => + type.GetDefinition()?.KnownTypeCode == KnownTypeCode.IReadOnlyListOfT) is not { } targetType) + { + return; + } + CollectionExpression collection; + if (!TryConvertExpression(objectCreateExpression, targetType, out collection)) + { + if (objectCreateExpression.Arguments.Count != 1) + return; + collection = CreateCollectionExpression( + new[] { (objectCreateExpression.Arguments.Single(), true) }, targetType); + } + context.Step("Use collection expression", objectCreateExpression); + objectCreateExpression.ReplaceWith(collection); + context.EndStep(collection); + } + + public override void VisitReturnStatement(ReturnStatement returnStatement) + { + base.VisitReturnStatement(returnStatement); + if (returnStatement.Expression == null) + return; + var method = returnStatement.Ancestors.OfType() + .Select(declaration => declaration.GetSymbol()) + .OfType() + .FirstOrDefault(); + if (method == null || !TryConvertExpression(returnStatement.Expression, method.ReturnType, out var collection)) + return; + context.Step("Use collection expression", returnStatement.Expression); + returnStatement.Expression.ReplaceWith(collection); + context.EndStep(collection); + } + + public override void VisitVariableDeclarationStatement(VariableDeclarationStatement variableDeclarationStatement) + { + base.VisitVariableDeclarationStatement(variableDeclarationStatement); + if (variableDeclarationStatement.Variables.Count != 1) + return; + var variable = variableDeclarationStatement.Variables.Single(); + if (variable.Initializer == null) + return; + var targetType = variableDeclarationStatement.Type.GetResolveResult().Type; + if (targetType.Kind == TypeKind.Unknown + || !TryConvertExpression(variable.Initializer, targetType, out var collection)) + { + return; + } + context.Step("Use collection expression", variable.Initializer); + variable.Initializer.ReplaceWith(collection); + context.EndStep(collection); + } + + bool TryTransformListConstruction(BlockStatement block, Statement statement) + { + if (statement is not VariableDeclarationStatement { Variables.Count: 1 } listDeclaration) + return false; + var listVariable = listDeclaration.Variables.Single(); + if (listVariable.Initializer is not ObjectCreateExpression listCreation + || listCreation.GetResolveResult().Type.GetDefinition()?.FullName != "System.Collections.Generic.List") + { + return false; + } + + var elements = new List<(Expression Expression, bool IsSpread)>(); + var setupStatements = new List { statement }; + if (statement.PrevSibling is VariableDeclarationStatement { Variables.Count: 1 } countDeclaration + && listCreation.Arguments.Count == 1 + && listCreation.Arguments.Single() is IdentifierExpression countReference + && countReference.Identifier == countDeclaration.Variables.Single().Name) + { + setupStatements.Insert(0, countDeclaration); + } + Statement? sink = statement.GetNextStatement(); + while (sink != null && TryMatchAdd(sink, listVariable.Name, out var element, out bool isSpread)) + { + elements.Add((element, isSpread)); + setupStatements.Add(sink); + sink = sink.GetNextStatement(); + } + + if (elements.Count == 0) + { + if (!TryParseSpanInitialization(listDeclaration, setupStatements, elements, ref sink)) + return false; + } + if (elements.Any(element => element.Expression.DescendantsAndSelf.OfType() + .Any(identifier => identifier.Identifier == listVariable.Name))) + { + return false; + } + if (sink is not ReturnStatement { Expression: { } returnExpression } + || !TryMatchCollectionSink(returnExpression, listVariable.Name, out var replaceTarget)) + { + return false; + } + var method = sink.Ancestors.OfType() + .Select(declaration => declaration.GetSymbol()) + .OfType() + .FirstOrDefault(); + if (method == null) + return false; + var collection = CreateCollectionExpression(elements, method.ReturnType); + context.Step("Reconstruct collection expression", statement); + replaceTarget.ReplaceWith(collection); + foreach (var setupStatement in setupStatements) + setupStatement.Remove(); + context.EndStep(collection); + return true; + } + + bool TryParseSpanInitialization(VariableDeclarationStatement listDeclaration, + List setupStatements, List<(Expression Expression, bool IsSpread)> elements, + ref Statement? sink) + { + var listName = listDeclaration.Variables.Single().Name; + var current = listDeclaration.GetNextStatement(); + bool hasSetCount = false; + string? spanName = null; + while (current != null) + { + if (current is ReturnStatement) + { + sink = current; + return hasSetCount && spanName != null && elements.Count > 0; + } + if (current.Descendants.OfType().Any(call => + call.Target is MemberReferenceExpression { MemberName: "SetCount" })) + { + hasSetCount = true; + setupStatements.Add(current); + current = current.GetNextStatement(); + continue; + } + if (current is VariableDeclarationStatement { Variables.Count: 1 } declaration) + { + var variable = declaration.Variables.Single(); + if (variable.Initializer is InvocationExpression { + Target: MemberReferenceExpression { MemberName: "AsSpan" } + }) + { + spanName = variable.Name; + } + else if (variable.Initializer is ObjectCreateExpression { Arguments.Count: 1 } spanCreation + && spanCreation.GetResolveResult().Type.GetDefinition()?.FullName == "System.ReadOnlySpan") + { + elements.Add((spanCreation.Arguments.Single(), true)); + } + else if (variable.Initializer?.GetResolveResult().IsCompileTimeConstant != true) + { + return false; + } + setupStatements.Add(current); + current = current.GetNextStatement(); + continue; + } + if (current is ForeachStatement foreachStatement) + { + if (spanName == null || !IsSpanCopyLoop(foreachStatement, spanName)) + return false; + elements.Add((foreachStatement.InExpression, true)); + setupStatements.Add(current); + current = current.GetNextStatement(); + continue; + } + if (spanName != null && current is ExpressionStatement { + Expression: AssignmentExpression { + Operator: AssignmentOperatorType.Assign, + Left: IndexerExpression { Target: IdentifierExpression spanTarget }, + Right: var value + } + } && spanTarget.Identifier == spanName) + { + elements.Add((value, false)); + setupStatements.Add(current); + current = current.GetNextStatement(); + continue; + } + if (current is ExpressionStatement expressionStatement + && (expressionStatement.Descendants.OfType().Any(call => + call.Target is MemberReferenceExpression { MemberName: "CopyTo" or "Slice" }) + || expressionStatement.Expression is AssignmentExpression { + Left: IdentifierExpression, + })) + { + setupStatements.Add(current); + current = current.GetNextStatement(); + continue; + } + return false; + } + return false; + + static bool IsSpanCopyLoop(ForeachStatement foreachStatement, string spanName) + { + var statements = foreachStatement.EmbeddedStatement is BlockStatement block + ? block.Statements.ToArray() + : new[] { foreachStatement.EmbeddedStatement }; + if (statements.Any(statement => statement is not ExpressionStatement)) + return false; + return statements.OfType().Any(statement => + statement.Expression is AssignmentExpression { + Operator: AssignmentOperatorType.Assign, + Left: IndexerExpression { Target: IdentifierExpression target } + } && target.Identifier == spanName); + } + } + + bool TryTransformInlineArrayConstruction(BlockStatement block, Statement statement) + { + if (statement is not VariableDeclarationStatement { Variables.Count: 1 } declaration) + return false; + var variable = declaration.Variables.Single(); + var inlineArrayType = declaration.Type.GetResolveResult().Type; + var definition = inlineArrayType.GetDefinition(); + if (definition == null || definition.GetInlineArrayLength() is not int length || length <= 0) + return false; + var values = new List(); + var assignments = new List(); + var current = statement.GetNextStatement(); + while (current is ExpressionStatement { + Expression: AssignmentExpression { + Operator: AssignmentOperatorType.Assign, + Left: IndexerExpression { Target: IdentifierExpression target }, + Right: var value + } + } && target.Identifier == variable.Name) + { + values.Add(value); + assignments.Add(current); + current = current.GetNextStatement(); + } + if (values.Count != length) + return false; + var spanDefinition = context.TypeSystem.FindType(KnownTypeCode.SpanOfT).GetDefinition(); + if (spanDefinition == null) + return false; + var elementType = inlineArrayType.GetInlineArrayElementType(); + var spanType = new ParameterizedType(spanDefinition, elementType); + var collection = CreateCollectionExpression(values.Select(value => (value, false)), spanType); + context.Step("Reconstruct span collection expression", statement); + declaration.Type.ReplaceWith(context.TypeSystemAstBuilder.ConvertType(spanType)); + variable.Initializer?.ReplaceWith(collection); + foreach (var assignment in assignments) + assignment.Remove(); + context.EndStep(collection); + return true; + } + + bool TryConvertExpression(Expression expression, IType targetType, out CollectionExpression collection) + { + collection = null!; + if (!TryExtractElements(expression, targetType, out var elements)) + return false; + collection = CreateCollectionExpression(elements.Select(element => (element, false)), targetType); + return true; + } + + bool TryExtractElements(Expression expression, IType targetType, out List elements) + { + elements = new List(); + if (expression is InvocationExpression { Arguments.Count: 0 } invocation + && invocation.GetSymbol() is IMethod { Name: "Empty", IsStatic: true, DeclaringType.FullName: "System.Array" }) + { + return true; + } + if (expression is CastExpression cast) + return TryExtractElements(cast.Expression, targetType, out elements); + if (expression is ArrayCreateExpression arrayCreation) + { + if (targetType is ArrayType { Dimensions: not 1 }) + return false; + if (arrayCreation.Initializer == null) + { + return arrayCreation.Arguments.Count == 0 + || (arrayCreation.Arguments.Count == 1 + && arrayCreation.Arguments.Single().GetResolveResult().ConstantValue is int length + && length == 0); + } + return ExtractInitializer(arrayCreation.Initializer, elements); + } + if (expression is ObjectCreateExpression objectCreation) + { + var fullName = objectCreation.GetResolveResult().Type.GetDefinition()?.FullName; + if (fullName == "System.Collections.Generic.List") + { + if (!ExtractInitializer(objectCreation.Initializer, elements)) + return false; + return objectCreation.Arguments.Count == 0 + || (objectCreation.Arguments.Count == 1 + && objectCreation.Arguments.Single().GetResolveResult().ConstantValue is int capacity + && capacity == elements.Count); + } + if (fullName?.StartsWith("<>z__ReadOnlySingleElementList", StringComparison.Ordinal) == true + && objectCreation.Arguments.Count == 1) + { + elements.Add(objectCreation.Arguments.Single()); + return true; + } + if ((fullName?.StartsWith("<>z__ReadOnlyArray", StringComparison.Ordinal) == true + || fullName?.StartsWith("<>z__ReadOnlyList", StringComparison.Ordinal) == true) + && objectCreation.Arguments.Count == 1) + { + return TryExtractElements(objectCreation.Arguments.Single(), targetType, out elements); + } + if (fullName == "System.ReadOnlySpan" && objectCreation.Arguments.Count == 1) + return TryExtractElements(objectCreation.Arguments.Single(), targetType, out elements); + } + if (expression is InvocationExpression builderCall + && targetType.GetDefinition()?.GetAttributes().Any(attribute => + attribute.AttributeType.ReflectionName == "System.Runtime.CompilerServices.CollectionBuilderAttribute") + == true + && builderCall.Arguments.Count == 1) + { + return TryExtractElements(builderCall.Arguments.Single(), targetType, out elements); + } + return false; + } + + static bool IsReadOnlyCollectionHelper(IType type) + { + return type.GetDefinition()?.FullName?.StartsWith("<>z__ReadOnly", StringComparison.Ordinal) == true; + } + + static bool ExtractInitializer(ArrayInitializerExpression? initializer, List elements) + { + if (initializer == null) + return true; + foreach (var element in initializer.Elements) + { + elements.Add(element is ArrayInitializerExpression { Elements.Count: 1 } nested + ? nested.Elements.Single() + : element); + } + return true; + } + + static bool TryMatchAdd(Statement statement, string variableName, + out Expression element, out bool isSpread) + { + element = null!; + isSpread = false; + if (statement is not ExpressionStatement { + Expression: InvocationExpression { + Target: MemberReferenceExpression { + Target: IdentifierExpression target, + MemberName: var methodName + }, + Arguments.Count: 1 + } invocation + } || target.Identifier != variableName || methodName is not ("Add" or "AddRange")) + { + return false; + } + element = invocation.Arguments.Single(); + isSpread = methodName == "AddRange"; + return true; + } + + static bool TryMatchCollectionSink(Expression expression, string variableName, out Expression replaceTarget) + { + replaceTarget = null!; + if (expression is IdentifierExpression identifier && identifier.Identifier == variableName) + { + replaceTarget = expression; + return true; + } + var references = expression.DescendantsAndSelf.OfType() + .Where(identifierExpression => identifierExpression.Identifier == variableName) + .ToArray(); + if (references.Length != 1) + return false; + if (expression is InvocationExpression { + Target: MemberReferenceExpression { Target: IdentifierExpression target, MemberName: "ToArray" }, + Arguments.Count: 0 + } && target.Identifier == variableName) + { + replaceTarget = expression; + return true; + } + if (expression is InvocationExpression builderCall + && builderCall.Descendants.OfType().Any(call => + call.Target is MemberReferenceExpression { Target: IdentifierExpression list, MemberName: "ToArray" } + && list.Identifier == variableName)) + { + replaceTarget = expression; + return true; + } + return false; + } + + static CollectionExpression CreateCollectionExpression( + IEnumerable<(Expression Expression, bool IsSpread)> elements, IType targetType) + { + var collection = new CollectionExpression(); + foreach (var element in elements) + { + collection.Elements.Add(element.IsSpread + ? new SpreadElement { Expression = element.Expression.Detach() } + : element.Expression.Detach()); + } + collection.AddAnnotation(new ResolveResult(targetType)); + return collection; + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs b/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs index dc4fa9a9ea..78e15eb194 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs @@ -215,7 +215,10 @@ public override void VisitIndexerDeclaration(IndexerDeclaration indexerDeclarati ReturnType = new AnyNode(), Getter = new Accessor() { Modifiers = Modifiers.Any, - Body = new BlockStatement() { new ReturnStatement(new AnyNode("expression")) } + Body = new BlockStatement() { + new Repeat(new EmptyStatement()).ToStatement(), + new ReturnStatement(new AnyNode("expression")) + } } }; @@ -227,7 +230,10 @@ public override void VisitIndexerDeclaration(IndexerDeclaration indexerDeclarati ReturnType = new AnyNode(), Getter = new Accessor() { Modifiers = Modifiers.Any, - Body = new BlockStatement() { new ReturnStatement(new AnyNode("expression")) } + Body = new BlockStatement() { + new Repeat(new EmptyStatement()).ToStatement(), + new ReturnStatement(new AnyNode("expression")) + } } }; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index 2f4adf6e40..6e7267431d 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -820,12 +820,14 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom } static readonly BlockStatement trivialFieldGetterBody = new BlockStatement { + new Repeat(new EmptyStatement()).ToStatement(), new ReturnStatement { Expression = new NamedNode("fieldReference", new IdentifierExpression("field")) } }; static readonly BlockStatement trivialFieldSetterBody = new BlockStatement { + new Repeat(new EmptyStatement()).ToStatement(), new AssignmentExpression { Left = new NamedNode("fieldReference", new IdentifierExpression("field")), Right = new IdentifierExpression("value") @@ -843,7 +845,8 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom { if (!TryGetBackingField(property, out var field)) return null; - if (!OutsideReferencesAreExpressible(propertyDeclaration, field)) + if (!OutsideReferencesAreExpressible(propertyDeclaration, field) + && !IsTrivialRecordAutoProperty(propertyDeclaration, property, field)) { // The field stays declared, so the "field" keyword references emitted by // ExpressionBuilder.ConvertField have to become ordinary field references again. @@ -867,7 +870,8 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom // pre-C# 14 transform recognizes them by their "_" backing field // instead. Keep that rule, or a VB auto-property grows explicit "field" // accessors where every other compiler's collapses to "{ get; set; }". - bool accessorsMustBeCompilerGenerated = field.Name != "_" + property.Name; + bool accessorsMustBeCompilerGenerated = field.Name != "_" + property.Name + && property.DeclaringTypeDefinition?.IsCompilerGenerated() != true; CollapseTrivialAccessor(getter, trivialFieldGetterBody, field, accessorsMustBeCompilerGenerated); // A readonly setter cannot become an auto-accessor (same rule as the pre-C# 14 // transform); readonly getters collapse fine because auto-getters are @@ -901,6 +905,28 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom return null; } + bool IsTrivialRecordAutoProperty(PropertyDeclaration declaration, IProperty property, IField field) + { + if (property.DeclaringTypeDefinition?.IsRecord != true) + return false; + return IsTrivialAccessor(declaration.Getter, trivialFieldGetterBody, field) + && IsTrivialAccessor(declaration.Setter, trivialFieldSetterBody, field); + } + + bool IsTrivialAccessor(Accessor? accessor, BlockStatement pattern, IField field) + { + if (accessor == null || accessor.Body == null) + return true; + if (accessor.GetSymbol() is not IMethod method || !method.IsCompilerGenerated()) + return false; + Match match = pattern.Match(accessor.Body); + if (!match.Success) + return false; + var symbol = match.Get("fieldReference").Single().GetSymbol(); + // 缺失依赖时,field 标识符可能没有解析结果;其语法只会由已确认的 backing field 生成。 + return symbol == null || symbol is IField referencedField && field.Equals(referencedField.MemberDefinition); + } + void CollapseTrivialAccessor(Accessor? accessor, BlockStatement pattern, IField field, bool accessorMustBeCompilerGenerated) { @@ -914,8 +940,10 @@ void CollapseTrivialAccessor(Accessor? accessor, BlockStatement pattern, IField Match m = pattern.Match(accessor.Body); if (!m.Success) return; - if (m.Get("fieldReference").Single().GetSymbol() is not IField referencedField - || !field.Equals(referencedField.MemberDefinition)) + var symbol = m.Get("fieldReference").Single().GetSymbol(); + // 缺失依赖时允许解析结果为空,但仍拒绝解析到其他成员的标识符。 + if (symbol != null && (symbol is not IField referencedField + || !field.Equals(referencedField.MemberDefinition))) { return; } @@ -937,13 +965,10 @@ internal static bool TryGetBackingField(IProperty property, [NotNullWhen(true)] var propertyType = ((IProperty)property.MemberDefinition).ReturnType; foreach (var candidate in property.DeclaringTypeDefinition.Fields) { - if (candidate.IsCompilerGenerated() + if ((candidate.IsCompilerGenerated() || property.DeclaringTypeDefinition.IsCompilerGenerated()) && candidate.IsStatic == property.IsStatic - // The trivial accessor bodies of a classic auto-property guaranteed this - // structurally; arbitrary accessor bodies do not. A field of a different - // type is not this property's storage, and removing it while printing - // `field` would substitute storage of the property's type instead. - && candidate.Type.Equals(propertyType) + // 正常情况下类型必须一致;缺失依赖导致占位类型不等时,改用精确的元数据映射确认关系。 + && BackingFieldTypeMatches(candidate, property, propertyType) && NameCouldBeBackingFieldOfAutomaticProperty(candidate.Name, out var propertyName) && propertyName == property.Name) { @@ -954,6 +979,21 @@ internal static bool TryGetBackingField(IProperty property, [NotNullWhen(true)] return false; } + static bool BackingFieldTypeMatches(IField candidate, IProperty property, IType propertyType) + { + if (candidate.Type.Equals(propertyType)) + return true; + if (candidate.ParentModule is not MetadataModule module + || candidate.MetadataToken.Kind != HandleKind.FieldDefinition + || property.MemberDefinition.MetadataToken.Kind != HandleKind.PropertyDefinition) + { + return false; + } + return module.MetadataFile.PropertyAndEventBackingFieldLookup.IsPropertyBackingField( + (FieldDefinitionHandle)candidate.MetadataToken, out var propertyHandle) + && propertyHandle == (PropertyDefinitionHandle)property.MemberDefinition.MetadataToken; + } + /// /// The "field" keyword cannot express backing-field accesses outside the owning /// property's accessors. The only shapes C# can express are stores in a constructor of @@ -1083,7 +1123,7 @@ internal static bool IsBackingFieldOfAutomaticProperty(IField field, [NotNullWhe property = null; if (!NameCouldBeBackingFieldOfAutomaticProperty(field.Name, out var propertyName)) return false; - if (!field.IsCompilerGenerated()) + if (!field.IsCompilerGenerated() && field.DeclaringTypeDefinition?.IsCompilerGenerated() != true) return false; property = field.DeclaringTypeDefinition? .GetProperties(p => p.Name == propertyName, GetMemberOptions.IgnoreInheritedMembers) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index b7d3795946..3cdc6e7614 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -43,27 +43,75 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms /// public class TransformFieldAndConstructorInitializers : IAstTransform { - /// - /// Pattern for reference types: - /// this..ctor(...); - /// - internal static readonly AstNode ThisCallClassPattern = new ExpressionStatement( - new NamedNode("invocation", new InvocationExpression( - new MemberReferenceExpression( - new Choice { - new NamedNode("target", new ThisReferenceExpression()), - new NamedNode("target", new BaseReferenceExpression()), - new CastExpression { - Type = new AnyNode(), - Expression = new Choice { - new NamedNode("target", new ThisReferenceExpression()), - new NamedNode("target", new BaseReferenceExpression()), - } - } - }, ".ctor"), - new Repeat(new AnyNode())) - ) - ); + static bool TryMatchClassConstructorCall(Statement? statement, + [NotNullWhen(true)] out InvocationExpression? invocation, + [NotNullWhen(true)] out Expression? target) + { + invocation = null; + target = null; + if (statement is not ExpressionStatement { + Expression: InvocationExpression { + Target: MemberReferenceExpression { MemberName: ".ctor" } memberReference + } candidate + }) + { + return false; + } + + var unwrappedTarget = UnwrapConstructorCallTarget(memberReference.Target); + if (unwrappedTarget is not (ThisReferenceExpression or BaseReferenceExpression)) + return false; + + invocation = candidate; + target = memberReference.Target; + return true; + } + + static Expression UnwrapConstructorCallTarget(Expression target) + { + while (true) + { + target = target switch { + ParenthesizedExpression parenthesized => parenthesized.Expression, + CastExpression cast => cast.Expression, + _ => target, + }; + if (target is not (ParenthesizedExpression or CastExpression)) + return target; + } + } + + static ConstructorInitializerType GetConstructorInitializerType( + InvocationExpression invocation, Expression target, IMethod ctorMethod) + { + if (invocation.GetSymbol() is IMethod { IsConstructor: true } ctor) + { + return ctor.DeclaringTypeDefinition == ctorMethod.DeclaringTypeDefinition + ? ConstructorInitializerType.This + : ConstructorInitializerType.Base; + } + + while (target is ParenthesizedExpression parenthesized) + target = parenthesized.Expression; + if (target is BaseReferenceExpression) + return ConstructorInitializerType.Base; + if (target is ThisReferenceExpression) + return ConstructorInitializerType.This; + if (target is CastExpression cast) + { + var innerTarget = UnwrapConstructorCallTarget(cast.Expression); + if (innerTarget is BaseReferenceExpression) + return ConstructorInitializerType.Base; + if (cast.GetResolveResult().Type.GetDefinition() == ctorMethod.DeclaringTypeDefinition) + return ConstructorInitializerType.This; + } + return ConstructorInitializerType.Base; + } + + static Statement? FirstNonEmptyStatement(IEnumerable statements) + { + return statements.FirstOrDefault(statement => statement is not EmptyStatement); + } /// /// Pattern for value types: @@ -173,12 +221,15 @@ class InitializerSequence } else { - var m = isStruct - ? ThisCallStructPattern.Match(stmt) - : ThisCallClassPattern.Match(stmt); - if (m.Success) + var constructorCallStatement = stmt; + while (constructorCallStatement is EmptyStatement) + constructorCallStatement = constructorCallStatement.GetNextStatement(); + bool isConstructorCall = isStruct + ? ThisCallStructPattern.IsMatch(constructorCallStatement) + : TryMatchClassConstructorCall(constructorCallStatement, out _, out _); + if (isConstructorCall) { - sequence.CoversFullBody = stmt.GetNextStatement() == null; + sequence.CoversFullBody = constructorCallStatement!.GetNextStatement() == null; } } } @@ -369,15 +420,20 @@ public bool Analyze(IEnumerable members) else { // find this-ctor call - var stmt = ctor.Body?.Statements.FirstOrDefault(); - var m = ctorMethod.DeclaringType.Kind == TypeKind.Struct - ? ThisCallStructPattern.Match(stmt) - : ThisCallClassPattern.Match(stmt); - + var stmt = ctor.Body == null ? null : FirstNonEmptyStatement(ctor.Body.Statements); allCtors.Add(ctor); - if (m.Success && m.Get("target").Single() is ThisReferenceExpression) + if (ctorMethod.DeclaringType.Kind == TypeKind.Struct) + { + var m = ThisCallStructPattern.Match(stmt); + if (m.Success && m.Get("target").Single() is ThisReferenceExpression) + continue; + } + else if (TryMatchClassConstructorCall(stmt, out var invocation, out var target) + && GetConstructorInitializerType(invocation, target, ctorMethod) == ConstructorInitializerType.This) + { continue; + } constructorsNotChainedWithThis.Add(ctor); } @@ -527,7 +583,7 @@ public bool MoveConstructorInitializer(ConstructorDeclaration constructorDeclara { if (constructorDeclaration.Body is null) return false; - Statement stmt = constructorDeclaration.Body.Statements.FirstOrDefault()!; + Statement stmt = FirstNonEmptyStatement(constructorDeclaration.Body.Statements)!; var isValueType = ctorMethod.DeclaringType.Kind == TypeKind.Struct; // value types may omit the constructor initializer completely @@ -536,22 +592,50 @@ public bool MoveConstructorInitializer(ConstructorDeclaration constructorDeclara return true; } - var m = isValueType - ? ThisCallStructPattern.Match(stmt) - : ThisCallClassPattern.Match(stmt); - - if (!m.Success) - return isValueType; - - Debug.Assert(stmt != null); // because m.Success - - AstNode invocation = m.Get("invocation").Single(); - if (invocation.GetSymbol() is not IMethod { IsConstructor: true } ctor) - return false; + AstNode invocation; + ConstructorInitializerType type; + if (isValueType) + { + var m = ThisCallStructPattern.Match(stmt); + if (!m.Success) + return true; + invocation = m.Get("invocation").Single(); + type = ConstructorInitializerType.This; + } + else + { + InvocationExpression? classInvocation; + Expression? classTarget; + if (!TryMatchClassConstructorCall(stmt, out classInvocation, out classTarget)) + { + foreach (var candidate in constructorDeclaration.Body.Statements.Skip(1)) + { + if (!TryMatchClassConstructorCall(candidate, out var candidateInvocation, out var candidateTarget)) + { + continue; + } + var candidateType = GetConstructorInitializerType(candidateInvocation, candidateTarget, ctorMethod); + if (!TryReconstructSpreadCollectionArgument(constructorDeclaration, candidate, candidateInvocation) + && !TryInlineConstructorCallTemporaries(constructorDeclaration, candidate, candidateInvocation) + && (candidateType != ConstructorInitializerType.Base || candidateInvocation.Arguments.Any())) + { + continue; + } + stmt = candidate; + classInvocation = candidateInvocation; + classTarget = candidateTarget; + break; + } + } + if (classInvocation == null || classTarget == null) + return false; + invocation = classInvocation; + type = GetConstructorInitializerType(classInvocation, classTarget, ctorMethod); + } - ConstructorInitializerType type = ctor.DeclaringTypeDefinition == ctorMethod.DeclaringTypeDefinition - ? ConstructorInitializerType.This - : ConstructorInitializerType.Base; + Debug.Assert(stmt != null); + if (context.Settings.CollectionExpressions) + ConvertConstructorInitializerCollections(invocation); var ci = new ConstructorInitializer { ConstructorInitializerType = type }; @@ -569,6 +653,290 @@ public bool MoveConstructorInitializer(ConstructorDeclaration constructorDeclara return true; } + void ConvertConstructorInitializerCollections(AstNode invocation) + { + foreach (var creation in invocation.Descendants.OfType().ToArray()) + { + if (creation.GetResolveResult().Type.GetDefinition()?.FullName != "System.Collections.Generic.List") + continue; + var elements = new List(); + if (creation.Initializer != null) + { + foreach (var element in creation.Initializer.Elements) + { + elements.Add(element is ArrayInitializerExpression { Elements.Count: 1 } nested + ? nested.Elements.Single() + : element); + } + } + if (creation.Arguments.Count > 1 + || (creation.Arguments.Count == 1 + && (creation.Arguments.Single().GetResolveResult().ConstantValue is not int capacity + || capacity != elements.Count))) + { + continue; + } + + var collection = new CollectionExpression(); + foreach (var element in elements) + collection.Elements.Add(element.Detach()); + AstNode replaceTarget = creation.Parent is CastExpression cast ? cast : creation; + collection.AddAnnotation(new ResolveResult(replaceTarget.GetResolveResult().Type)); + context.Step("Use collection expression in constructor initializer", creation); + replaceTarget.ReplaceWith(collection); + context.EndStep(collection); + } + } + + bool TryReconstructSpreadCollectionArgument(ConstructorDeclaration constructorDeclaration, + Statement constructorCallStatement, InvocationExpression invocation) + { + if (!context.Settings.CollectionExpressions) + return false; + Debug.Assert(constructorDeclaration.Body != null); + var precedingStatements = constructorDeclaration.Body.Statements + .TakeWhile(statement => statement != constructorCallStatement) + .Where(statement => statement is not EmptyStatement) + .ToList(); + if (precedingStatements.Count < 2) + return false; + + VariableDeclarationStatement? listDeclaration = null; + VariableInitializer? listVariable = null; + ObjectCreateExpression? listCreation = null; + int listDeclarationIndex = -1; + for (int i = 0; i < precedingStatements.Count; i++) + { + if (precedingStatements[i] is not VariableDeclarationStatement { Variables.Count: 1 } declaration) + continue; + var variable = declaration.Variables.Single(); + if (variable.Initializer is not ObjectCreateExpression creation + || creation.GetResolveResult().Type.GetDefinition()?.FullName != "System.Collections.Generic.List") + { + continue; + } + var references = invocation.Descendants.OfType() + .Where(identifier => identifier.Identifier == variable.Name) + .ToArray(); + if (references.Length != 1) + continue; + listDeclaration = declaration; + listVariable = variable; + listCreation = creation; + listDeclarationIndex = i; + break; + } + if (listDeclaration == null || listVariable == null || listCreation == null) + return false; + + var elements = new List<(Expression Expression, bool IsSpread)>(); + if (!TryParseAddSequence() && !TryParseSpanSequence()) + return false; + if (elements.Any(element => element.Expression.DescendantsAndSelf + .Any(node => node is ThisReferenceExpression or BaseReferenceExpression))) + { + return false; + } + var listReferences = constructorDeclaration.Body.Descendants.OfType() + .Where(identifier => identifier.Identifier == listVariable.Name) + .ToArray(); + if (listReferences.Any(reference => !precedingStatements.Any(statement => reference.Ancestors.Contains(statement)) + && !reference.Ancestors.Contains(invocation))) + { + return false; + } + + var invocationReference = invocation.Descendants.OfType() + .Single(identifier => identifier.Identifier == listVariable.Name); + AstNode replaceTarget = invocationReference.Parent is CastExpression cast ? cast : invocationReference; + var collection = new CollectionExpression(); + foreach (var element in elements) + { + collection.Elements.Add(element.IsSpread + ? new SpreadElement { Expression = element.Expression.Detach() } + : element.Expression.Detach()); + } + collection.AddAnnotation(new ResolveResult(replaceTarget.GetResolveResult().Type)); + + context.Step("Reconstruct spread collection in constructor initializer", constructorCallStatement); + replaceTarget.ReplaceWith(collection); + foreach (var statement in precedingStatements) + statement.Remove(); + context.EndStep(collection); + return true; + + bool TryParseAddSequence() + { + if (listDeclarationIndex != 0) + return false; + foreach (var statement in precedingStatements.Skip(1)) + { + if (statement is not ExpressionStatement { + Expression: InvocationExpression { + Target: MemberReferenceExpression { + Target: IdentifierExpression target, + MemberName: var methodName + }, + Arguments.Count: 1 + } call + } || target.Identifier != listVariable.Name || methodName is not ("Add" or "AddRange")) + { + elements.Clear(); + return false; + } + elements.Add((call.Arguments.Single(), methodName == "AddRange")); + } + return true; + } + + bool TryParseSpanSequence() + { + elements.Clear(); + if (precedingStatements.Take(listDeclarationIndex) + .Any(statement => statement is not VariableDeclarationStatement { Variables.Count: 1 })) + { + return false; + } + var followingStatements = precedingStatements.Skip(listDeclarationIndex + 1).ToArray(); + bool hasSetCount = followingStatements + .SelectMany(statement => statement.Descendants.OfType()) + .Any(call => call.Target is MemberReferenceExpression { MemberName: "SetCount" }); + var spanDeclaration = followingStatements.OfType() + .FirstOrDefault(declaration => declaration is { Variables.Count: 1 } + && declaration.Variables.Single().Initializer is InvocationExpression { + Target: MemberReferenceExpression { MemberName: "AsSpan" } + }); + if (!hasSetCount || spanDeclaration == null) + return false; + var spanName = spanDeclaration.Variables.Single().Name; + bool afterSpanDeclaration = false; + foreach (var statement in followingStatements) + { + if (statement == spanDeclaration) + { + afterSpanDeclaration = true; + continue; + } + if (!afterSpanDeclaration) + continue; + if (statement is ForeachStatement foreachStatement) + { + elements.Add((foreachStatement.InExpression, true)); + continue; + } + if (statement is VariableDeclarationStatement { Variables.Count: 1 } declaration + && declaration.Variables.Single().Initializer is ObjectCreateExpression { + Arguments.Count: 1 + } readOnlySpanCreation + && readOnlySpanCreation.GetResolveResult().Type.GetDefinition()?.FullName == "System.ReadOnlySpan") + { + elements.Add((readOnlySpanCreation.Arguments.Single(), true)); + continue; + } + if (statement is ExpressionStatement { + Expression: AssignmentExpression { + Operator: AssignmentOperatorType.Assign, + Left: IndexerExpression { Target: IdentifierExpression spanTarget }, + Right: var value + } + } && spanTarget.Identifier == spanName) + { + elements.Add((value, false)); + continue; + } + if (statement.Descendants.OfType().Any(call => + call.Target is MemberReferenceExpression { MemberName: "CopyTo" or "Slice" })) + { + continue; + } + if (statement is VariableDeclarationStatement or ExpressionStatement) + continue; + elements.Clear(); + return false; + } + return elements.Count > 0; + } + } + + bool TryInlineConstructorCallTemporaries(ConstructorDeclaration constructorDeclaration, + Statement constructorCallStatement, InvocationExpression invocation) + { + Debug.Assert(constructorDeclaration.Body != null); + var precedingStatements = constructorDeclaration.Body.Statements + .TakeWhile(statement => statement != constructorCallStatement) + .Where(statement => statement is not EmptyStatement) + .ToArray(); + if (precedingStatements.Length == 0 + || invocation.GetResolveResult() is not InvocationResolveResult { + Member: IMethod { SymbolKind: SymbolKind.Constructor } ctor + }) + { + return false; + } + + var namedArguments = invocation.Arguments.OfType().ToArray(); + if (namedArguments.Length != invocation.Arguments.Count + || namedArguments.Length != ctor.Parameters.Count + || namedArguments.Select(argument => argument.Name).Distinct().Count() != namedArguments.Length + || namedArguments.Any(argument => !ctor.Parameters.Any(parameter => parameter.Name == argument.Name))) + { + return false; + } + + var temporaries = new List<(VariableDeclarationStatement Declaration, + NamedArgumentExpression Argument, AstNode ReplacementTarget, Expression Initializer)>(); + foreach (var statement in precedingStatements) + { + if (statement is not VariableDeclarationStatement { Variables.Count: 1 } declaration) + return false; + var variable = declaration.Variables.Single(); + if (variable.Initializer is not Expression initializer || initializer.DescendantsAndSelf + .Any(node => node is ThisReferenceExpression or BaseReferenceExpression)) + { + return false; + } + + var ilVariable = variable.GetILVariable(); + var references = constructorDeclaration.Body.Descendants.OfType() + .Where(reference => ilVariable != null + ? reference.GetILVariable() == ilVariable + : reference.Identifier == variable.Name) + .ToArray(); + if (references.Length != 1) + return false; + var reference = references[0]; + var argument = reference.Ancestors.OfType().FirstOrDefault(); + if (argument == null || !namedArguments.Contains(argument)) + return false; + + AstNode replacementTarget = reference; + if (reference.Parent is DirectionExpression direction) + { + if (direction.FieldDirection != FieldDirection.In) + return false; + replacementTarget = direction; + } + temporaries.Add((declaration, argument, replacementTarget, initializer)); + } + if (temporaries.Select(temporary => temporary.Argument).Distinct().Count() != temporaries.Count) + return false; + + var reorderedArguments = temporaries.Select(temporary => temporary.Argument) + .Concat(namedArguments.Where(argument => temporaries.All(temporary => temporary.Argument != argument))) + .ToArray(); + context.Step("Inline constructor call temporaries", constructorCallStatement); + foreach (var temporary in temporaries) + temporary.ReplacementTarget.ReplaceWith(temporary.Initializer.Detach()); + foreach (var argument in reorderedArguments) + argument.Remove(); + foreach (var argument in reorderedArguments) + invocation.Arguments.Add(argument); + foreach (var temporary in temporaries) + temporary.Declaration.Remove(); + context.EndStep(invocation); + return true; + } + public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, InitializerKind kind) { foreach (var (stmt, member, initializer, dependsOnBody) in sequence.Statements) @@ -934,7 +1302,13 @@ private bool TransformDeclaration(ITypeDefinition currentTypeDefinition, AstNode var analyzer = new ConstructorInitializerAnalyzer(context, currentTypeDefinition, node as TypeDeclaration); if (!analyzer.Analyze(members)) + { + foreach (var constructorDeclaration in members.OfType()) + { + analyzer.MoveConstructorInitializer(constructorDeclaration, (IMethod)constructorDeclaration.GetSymbol()!); + } return false; + } if (analyzer.PrimaryConstructorInitializers is { HasDuplicateAssignments: false }) { diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index 580b393bd8..a43a52ace1 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -68,6 +68,9 @@ public class PortablePdbWriter /// public bool EmbedSourceFiles { get; set; } = true; + /// Provides the exact source text used for a PDB document, when available. + public Func SourceTextProvider { get; set; } + public static bool HasCodeViewDebugDirectoryEntry(PEFile file) { return file != null && file.Reader.ReadDebugDirectory().Any(entry => entry.Type == DebugDirectoryEntryType.CodeView); @@ -110,10 +113,13 @@ public void WritePdb( string BuildFileNameFromTypeName(TypeDefinitionHandle handle) { var typeName = handle.GetFullTypeName(reader).TopLevelTypeName; + string fileName = WholeProjectDecompiler.CleanUpFileName(typeName.Name, ".cs"); + if (string.IsNullOrEmpty(typeName.Namespace)) + return fileName; string ns = settings.UseNestedDirectoriesForNamespaces ? WholeProjectDecompiler.CleanUpPath(typeName.Namespace) : WholeProjectDecompiler.CleanUpDirectoryName(typeName.Namespace); - return Path.Combine(ns, WholeProjectDecompiler.CleanUpFileName(typeName.Name, ".cs")); + return Path.Combine(ns, fileName); } var sourceFiles = reader.GetTopLevelTypeDefinitions().Where(t => IncludeTypeWhenGeneratingPdb(file, t, settings)).GroupBy(BuildFileNameFromTypeName).ToList(); @@ -140,7 +146,8 @@ string BuildFileNameFromTypeName(TypeDefinitionHandle handle) // Generate source and checksum if (!NoLogo) syntaxTree.PrependLeadingTrivia(new Comment(" PDB and source generated by ICSharpCode.Decompiler " + decompilerVersion)); - var sourceText = SyntaxTreeToString(syntaxTree, settings); + var generatedSourceText = SyntaxTreeToString(syntaxTree, settings); + var sourceText = SourceTextProvider?.Invoke(sourceFile.Key) ?? generatedSourceText; // Generate sequence points for the syntax tree var sequencePoints = decompiler.CreateSequencePoints(syntaxTree); diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 3a5f0537b5..2b595acb5f 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -848,6 +848,13 @@ public bool LifetimeAnnotations { [DecompilerSetting(CSharp.LanguageVersion.CSharp12_0)] public partial bool InlineArrays { get; set; } + /// + /// 获取或设置是否还原 C# 12.0 集合表达式。 + /// + [Description("DecompilerSettings.CollectionExpressions")] + [DecompilerSetting(CSharp.LanguageVersion.CSharp12_0)] + public partial bool CollectionExpressions { get; set; } + /// /// Gets/Sets whether C# 14.0 extension members should be transformed. /// diff --git a/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs b/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs index d24cd5a0f8..9ca3e8bfc4 100644 --- a/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs +++ b/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs @@ -21,6 +21,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Text; @@ -75,12 +76,14 @@ public DotNetCorePackageInfo(string fullName, string type, string path, string[] readonly List searchPaths = new List(); readonly List packageBasePaths = new List(); readonly Version targetFrameworkVersion; + readonly TargetFrameworkIdentifier targetFramework; readonly string dotnetBasePath = FindDotNetExeDirectory(); readonly string preferredRuntimePack; public DotNetCorePathFinder(TargetFrameworkIdentifier targetFramework, Version targetFrameworkVersion, string preferredRuntimePack) { + this.targetFramework = targetFramework; this.targetFrameworkVersion = targetFrameworkVersion; this.preferredRuntimePack = preferredRuntimePack; @@ -212,6 +215,11 @@ static IEnumerable LoadPackageInfos(string depsJsonFileNa } public string TryResolveDotNetCoreShared(IAssemblyReference name, out string runtimePack) + { + return TryResolveDotNetCoreShared(name, out runtimePack, allowRollForward: true); + } + + internal string TryResolveDotNetCoreShared(IAssemblyReference name, out string runtimePack, bool allowRollForward) { if (dotnetBasePath == null) { @@ -232,14 +240,33 @@ public string TryResolveDotNetCoreShared(IAssemblyReference name, out string run string basePath = Path.Combine(dotnetBasePath, "shared", pack); if (!Directory.Exists(basePath)) continue; - var closestVersion = GetClosestVersionFolder(basePath, targetFrameworkVersion); - if (File.Exists(Path.Combine(basePath, closestVersion, name.Name + ".dll"))) + var versionFolders = new DirectoryInfo(basePath).GetDirectories() + .Select(ConvertToVersion) + .Where(v => v.version != null); + versionFolders = allowRollForward + ? versionFolders.Where(v => v.version >= targetFrameworkVersion) + : versionFolders.Where(v => v.version.Major == targetFrameworkVersion.Major + && v.version.Minor == targetFrameworkVersion.Minor + && v.version >= targetFrameworkVersion); + foreach (var folder in versionFolders.OrderBy(v => v.version)) { - return Path.Combine(basePath, closestVersion, name.Name + ".dll"); - } - else if (File.Exists(Path.Combine(basePath, closestVersion, name.Name + ".exe"))) - { - return Path.Combine(basePath, closestVersion, name.Name + ".exe"); + foreach (string extension in new[] { ".dll", ".exe" }) + { + string path = Path.Combine(folder.directory.FullName, name.Name + extension); + if (!File.Exists(path)) + continue; + try + { + var resolvedVersion = AssemblyName.GetAssemblyName(path).Version; + if (targetFramework == TargetFrameworkIdentifier.NETStandard + || name.Version == null || resolvedVersion == null || resolvedVersion >= name.Version) + return path; + } + catch (Exception ex) + { + Trace.TraceWarning(ex.ToString()); + } + } } } runtimePack = null; diff --git a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs index f72c953969..9e89017cfc 100644 --- a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs +++ b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs @@ -267,7 +267,8 @@ internal static (TargetFrameworkIdentifier, Version) ParseTargetFramework(string public override bool IsSharedAssembly(IAssemblyReference reference, [NotNullWhen(true)] out string? runtimePack) { - return dotNetCorePathFinder.Value.TryResolveDotNetCoreShared(reference, out runtimePack) != null; + return dotNetCorePathFinder.Value.TryResolveDotNetCoreShared( + reference, out runtimePack, allowRollForward: false) != null; } public string? FindAssemblyFile(IAssemblyReference name) @@ -297,22 +298,19 @@ public override bool IsSharedAssembly(IAssemblyReference reference, [NotNullWhen return FindWindowsMetadataFile(name); } - string? file; + if (UsesDotNetCorePathFinder(targetFrameworkIdentifier) && !IsZeroOrAllOnes(targetFrameworkVersion)) + { + string? file = dotNetCorePathFinder.Value.TryResolveDotNetCore(name); + if (file != null) + return file; + } + switch (targetFrameworkIdentifier) { - case TargetFrameworkIdentifier.NET: - case TargetFrameworkIdentifier.NETCoreApp: - case TargetFrameworkIdentifier.NETStandard: - if (IsZeroOrAllOnes(targetFrameworkVersion)) - goto default; - file = dotNetCorePathFinder.Value.TryResolveDotNetCore(name); - if (file != null) - return file; - goto default; case TargetFrameworkIdentifier.Silverlight: if (IsZeroOrAllOnes(targetFrameworkVersion)) goto default; - file = ResolveSilverlight(name, targetFrameworkVersion); + string? file = ResolveSilverlight(name, targetFrameworkVersion); if (file != null) return file; goto default; @@ -321,6 +319,12 @@ public override bool IsSharedAssembly(IAssemblyReference reference, [NotNullWhen } } + internal static bool UsesDotNetCorePathFinder(TargetFrameworkIdentifier identifier) + { + return identifier is TargetFrameworkIdentifier.NET or TargetFrameworkIdentifier.NETCoreApp + or TargetFrameworkIdentifier.NETStandard; + } + DotNetCorePathFinder InitDotNetCorePathFinder() { DotNetCorePathFinder dotNetCorePathFinder; @@ -486,7 +490,8 @@ string FindClosestVersionDirectory(string basePath, Version? version) return assembly; } - if (decompilerRuntime == DecompilerRuntime.NETCoreApp) + if (decompilerRuntime == DecompilerRuntime.NETCoreApp + && ShouldUseHostRuntimeFallback(targetFrameworkIdentifier, targetFramework, Environment.OSVersion.Platform)) { // Hosts without a .NET Framework installation (e.g. Linux, macOS) have no GAC; // the only system-wide assembly store there is the shared-framework directory @@ -505,6 +510,14 @@ string FindClosestVersionDirectory(string basePath, Version? version) return null; } + internal static bool ShouldUseHostRuntimeFallback(TargetFrameworkIdentifier identifier, + string targetFramework, PlatformID platform) + { + return platform != PlatformID.Win32NT + || (identifier != TargetFrameworkIdentifier.NETStandard + && !targetFramework.StartsWith(".NETFramework,", StringComparison.Ordinal)); + } + #region .NET / mono GAC handling string? SearchDirectory(IAssemblyReference name, IEnumerable directories) { diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 4ff2f25052..1b5170c2e6 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -106,7 +106,7 @@ static void WriteProjects(TextWriter writer, List projects, string static List WriteSolutionConfigurations(TextWriter writer, List projects) { - var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); + var platforms = projects.Select(p => GetSolutionPlatformName(p.PlatformName)).Distinct().ToList(); platforms.Sort(); @@ -139,20 +139,25 @@ static void WriteProjectConfigurations( foreach (var platform in solutionPlatforms) { - writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.ActiveCfg = Debug|{project.PlatformName}"); - writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.Build.0 = Debug|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.ActiveCfg = Debug|{GetSolutionPlatformName(project.PlatformName)}"); + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.Build.0 = Debug|{GetSolutionPlatformName(project.PlatformName)}"); } foreach (var platform in solutionPlatforms) { - writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.ActiveCfg = Release|{project.PlatformName}"); - writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.Build.0 = Release|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.ActiveCfg = Release|{GetSolutionPlatformName(project.PlatformName)}"); + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.Build.0 = Release|{GetSolutionPlatformName(project.PlatformName)}"); } } writer.WriteLine("\tEndGlobalSection"); } + static string GetSolutionPlatformName(string platformName) + { + return platformName == "AnyCPU" ? "Any CPU" : platformName; + } + static void FixAllProjectReferences(List projects) { var projectsMap = projects.ToDictionary( diff --git a/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs b/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs index 942a970e9f..6ffde43599 100644 --- a/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs +++ b/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs @@ -95,7 +95,8 @@ public async Task Project_Mode_Writes_Csproj_And_Cs() new DecompilerSettings(), Language(), progress, CancellationToken.None); result.Success.Should().BeTrue(result.StatusText); - Directory.EnumerateFiles(tempDir, "*.csproj").Should().HaveCount(1); + var projectFile = Directory.EnumerateFiles(tempDir, "*.csproj").Single(); + (await File.ReadAllTextAsync(projectFile)).Should().Contain("enable"); Directory.EnumerateFiles(tempDir, "*.cs", SearchOption.AllDirectories).Should().NotBeEmpty(); progress.Reports.Should().Contain(p => p.TotalUnits > 0, "project export reports a determinate per-file unit count to the progress sink"); diff --git a/ILSpy.Tests/Languages/ProjectExportTests.cs b/ILSpy.Tests/Languages/ProjectExportTests.cs index f0165b1840..2a07717466 100644 --- a/ILSpy.Tests/Languages/ProjectExportTests.cs +++ b/ILSpy.Tests/Languages/ProjectExportTests.cs @@ -26,6 +26,8 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Solution; using ICSharpCode.ILSpy.AppEnv; @@ -49,6 +51,37 @@ namespace ICSharpCode.ILSpy.Tests.Languages; [TestFixture] public class ProjectExportTests { + [AvaloniaTest] + public async Task ProjectPdbUsesProjectSourceTransformsAndFormatting() + { + var (_, vm) = await TestHarness.BootAsync(); + var loaded = await vm.OpenFixtureAsync(); + var file = (PEFile)loaded.GetMetadataFileOrNull()!; + var settings = new DecompilerSettings(); + var decompiler = ProjectExporter.CreateProjectPdbDecompiler(loaded, file, settings, default); + decompiler.AstTransforms.Should().Contain(t => t is EscapeInvalidIdentifiers); + decompiler.AstTransforms.Should().Contain(t => t is RemoveCLSCompliantAttribute); + + var options = new ProjectExportOptions("unused", UseSdkStyleProjectFormat: true, + UseNestedDirectoriesForNamespaces: false, RemoveDeadCode: false, RemoveDeadStores: false, + UseDebugSymbols: false, StrongNameKeyFile: null, GeneratePdb: true, + EmbedSourceFilesInPdb: false); + var sourceDirectory = Path.Combine(Path.GetTempPath(), "ILSpyPdbSource_" + System.Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(sourceDirectory); + try + { + await File.WriteAllTextAsync(Path.Combine(sourceDirectory, "Source.cs"), "source text"); + var writer = ProjectExporter.CreateProjectPdbWriter(options, sourceDirectory); + writer.NoLogo.Should().BeTrue(); + writer.EmbedSourceFiles.Should().BeFalse(); + writer.SourceTextProvider!("Source.cs").Should().Be("source text"); + } + finally + { + Directory.Delete(sourceDirectory, recursive: true); + } + } + [Test] public void Language_Base_Defaults_ProjectFileExtension_To_Null() { diff --git a/ILSpy.Tests/Languages/SolutionExportTests.cs b/ILSpy.Tests/Languages/SolutionExportTests.cs index 7a952bc5c5..98d63b894c 100644 --- a/ILSpy.Tests/Languages/SolutionExportTests.cs +++ b/ILSpy.Tests/Languages/SolutionExportTests.cs @@ -67,6 +67,8 @@ await vm.OpenFixtureAsync("FixtureB"), result.Success.Should().BeTrue( "the solution export should succeed for valid assemblies. Status:\n" + result.StatusText); File.Exists(slnPath).Should().BeTrue("the .sln file must be written to the chosen path"); + var solutionText = await File.ReadAllTextAsync(slnPath); + solutionText.Should().Contain("Debug|Any CPU").And.NotContain("Debug|AnyCPU"); foreach (var a in assemblies) { diff --git a/ILSpy/Commands/ProjectExporter.cs b/ILSpy/Commands/ProjectExporter.cs index 207ba8bcc9..6459dfa513 100644 --- a/ILSpy/Commands/ProjectExporter.cs +++ b/ILSpy/Commands/ProjectExporter.cs @@ -27,6 +27,7 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; +using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpyX; @@ -216,11 +217,8 @@ static void GeneratePdbs(IReadOnlyList assemblies, try { using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write); - var resolver = assembly.GetAssemblyResolver(); - var decompiler = new CSharpDecompiler(file, resolver, settingsClone) { - CancellationToken = ct, - }; - new PortablePdbWriter { EmbedSourceFiles = options.EmbedSourceFilesInPdb } + var decompiler = CreateProjectPdbDecompiler(assembly, file, settingsClone, ct); + CreateProjectPdbWriter(options, projectDirectory(assembly)) .WritePdb(file, decompiler, settingsClone, stream); report.AppendLine("Generated PDB: " + pdbFileName); } @@ -235,6 +233,30 @@ static void GeneratePdbs(IReadOnlyList assemblies, } } + internal static CSharpDecompiler CreateProjectPdbDecompiler(LoadedAssembly assembly, PEFile file, + DecompilerSettings settings, CancellationToken cancellationToken) + { + var decompiler = new CSharpDecompiler(file, assembly.GetAssemblyResolver(), settings) { + CancellationToken = cancellationToken, + DebugInfoProvider = assembly.GetDebugInfoOrNull(), + }; + decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); + decompiler.AstTransforms.Add(new RemoveCLSCompliantAttribute()); + return decompiler; + } + + internal static PortablePdbWriter CreateProjectPdbWriter(ProjectExportOptions options, string sourceDirectory) + { + return new PortablePdbWriter { + NoLogo = true, + EmbedSourceFiles = options.EmbedSourceFilesInPdb, + SourceTextProvider = fileName => { + string path = Path.Combine(sourceDirectory, fileName); + return File.Exists(path) ? File.ReadAllText(path) : null; + }, + }; + } + static void ApplyOverrides(DecompilerSettings settings, ProjectExportOptions options) { settings.UseSdkStyleProjectFormat = options.UseSdkStyleProjectFormat; diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 73d84bc0df..e7340ea91b 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -360,6 +360,9 @@ Are you sure you want to continue? User-defined checked operators + + Use collection expressions + Covariant return types