diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
new file mode 100644
index 0000000..a70de1e
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -0,0 +1,309 @@
+
+
+
+
+ net11.0
+ preview
+
+ $(NETCoreSdkRuntimeIdentifier)
+ win-x64;win-arm64;linux-x64;osx-arm64
+
+ false
+
+ true
+ ICSharpCode.Decompiler.Tests.MicrosoftTestingPlatformEntryPoint
+ Exe
+
+ True
+
+
+ true
+
+
+ $(NoWarn);1701;1702;1705,67,169,1058,728,1720,649,168,251,660,661,675;1998;162;8632;626;8618;8714;8602;8981;NU1902;NU1903;CA1416
+ ROSLYN;ROSLYN2;ROSLYN3;ROSLYN4;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100;CS110;CS120;CS130
+
+ False
+ False
+ False
+
+ false
+
+ True
+
+ True
+ ..\ICSharpCode.Decompiler\ICSharpCode.Decompiler.snk
+
+
+
+ full
+ true
+
+
+
+ pdbonly
+ true
+
+
+
+ TRACE;DEBUG;$(DefineConstants)
+
+
+
+ TRACE;$(DefineConstants)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
new file mode 100644
index 0000000..a700d4e
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
@@ -0,0 +1,203 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+
+using ICSharpCode.Decompiler.Tests.Helpers;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.Decompiler.Tests
+{
+ [TestFixture, Parallelizable(ParallelScope.All)]
+ public class VBPrettyTestRunner
+ {
+ static readonly string TestCasePath = Tester.TestCasePath + "/VBPretty";
+
+ [Test]
+ public void AllFilesHaveTests()
+ {
+ var testNames = typeof(VBPrettyTestRunner).GetMethods()
+ .Where(m => m.GetCustomAttributes(typeof(TestAttribute), false).Any())
+ .Select(m => m.Name)
+ .ToArray();
+ foreach (var file in new DirectoryInfo(TestCasePath).EnumerateFiles())
+ {
+ if (file.Extension.Equals(".vb", StringComparison.OrdinalIgnoreCase))
+ {
+ var testName = file.Name.Split('.')[0];
+ Assert.That(testNames, Has.Member(testName));
+ Assert.That(File.Exists(Path.Combine(TestCasePath, testName + ".cs")));
+ }
+ }
+ }
+
+ static readonly CompilerOptions[] defaultOptions = Tester.SupportedOnCurrentPlatform(new[]
+ {
+ CompilerOptions.None,
+ CompilerOptions.Optimize,
+ CompilerOptions.UseRoslyn1_3_2 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn1_3_2 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslynLatest | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn1_3_2,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn1_3_2,
+ CompilerOptions.UseRoslyn2_10_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn2_10_0,
+ CompilerOptions.UseRoslyn3_11_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn3_11_0,
+ CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.UseRoslynLatest,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
+ });
+
+ static readonly CompilerOptions[] roslynOnlyOptions = Tester.SupportedOnCurrentPlatform(new[]
+ {
+ CompilerOptions.UseRoslyn1_3_2 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn1_3_2 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslynLatest | CompilerOptions.TargetNet40,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest | CompilerOptions.TargetNet40,
+ CompilerOptions.UseRoslyn1_3_2,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn1_3_2,
+ CompilerOptions.UseRoslyn2_10_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn2_10_0,
+ CompilerOptions.UseRoslyn3_11_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn3_11_0,
+ CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.UseRoslynLatest,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
+ });
+
+ [Test]
+ public async Task Async([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test] // TODO: legacy VB compound assign
+ public async Task VBCompoundAssign([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task ParameterizedProperties([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task Select([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task VBAnonymousTypes([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ IgnoreIfVbRuntimeSubstituted(options);
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task Issue1906([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task Issue2192([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ IgnoreIfVbRuntimeSubstituted(options);
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task VBPropertiesTest([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task VBAutomaticEvents([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task VBNonGenericForEach([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ IgnoreIfVbRuntimeSubstituted(options);
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ [Test]
+ public async Task YieldReturn([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
+ static void IgnoreIfVbRuntimeSubstituted(CompilerOptions options)
+ {
+ if (!OperatingSystem.IsWindows()
+ && (options & CompilerOptions.UseRoslynMask) == CompilerOptions.UseRoslyn2_10_0
+ && (options & CompilerOptions.TargetNet40) == 0)
+ {
+ Assert.Ignore("The non-Windows netcore-2.2 configuration substitutes the legacy " +
+ "Microsoft.VisualBasic as -vbruntime (see Tester.CompileVB); the resulting " +
+ "reference graph changes this test's decompiled output.");
+ }
+ }
+
+ async Task Run([CallerMemberName] string testName = null, CompilerOptions options = CompilerOptions.UseDebug, DecompilerSettings settings = null)
+ {
+ var vbFile = Path.Combine(TestCasePath, testName + ".vb");
+ var csFile = Path.Combine(TestCasePath, testName + ".cs");
+ var exeFile = TestsAssemblyOutput.GetFilePath(TestCasePath, testName, Tester.GetSuffix(options) + ".exe");
+ if (options.HasFlag(CompilerOptions.Library))
+ {
+ exeFile = Path.ChangeExtension(exeFile, ".dll");
+ }
+
+ var executable = await Tester.CompileVB(vbFile, options | CompilerOptions.ReferenceVisualBasic, exeFile).ConfigureAwait(false);
+ settings ??= new DecompilerSettings { FileScopedNamespaces = false, SortCustomAttributes = true };
+ 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/IL/Transforms/DetectCatchWhenConditionBlocks.cs b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs
new file mode 100644
index 0000000..5aa9064
--- /dev/null
+++ b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs
@@ -0,0 +1,244 @@
+// Copyright (c) 2017 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Linq;
+
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace ICSharpCode.Decompiler.IL.Transforms
+{
+ public class DetectCatchWhenConditionBlocks : IILTransform
+ {
+ public void Run(ILFunction function, ILTransformContext context)
+ {
+ foreach (var catchBlock in function.Descendants.OfType())
+ {
+ if (catchBlock.Filter is BlockContainer container
+ && MatchCatchWhenEntryPoint(catchBlock.Variable, container, container.EntryPoint,
+ out var exceptionType, out var exceptionSlot, out var whenConditionBlock)
+ && exceptionType.GetStackType() == catchBlock.Variable.StackType)
+ {
+ // set exceptionType
+ catchBlock.Variable.Type = exceptionType;
+ // Block entryPoint (incoming: 1) {
+ // stloc temp(isinst exceptionType(ldloc exceptionVar))
+ // if (comp(ldloc temp != ldnull)) br whenConditionBlock
+ // br falseBlock
+ // }
+ // =>
+ // Block entryPoint (incoming: 1) {
+ // stloc temp(ldloc exceptionSlot)
+ // br whenConditionBlock
+ // }
+ var instructions = container.EntryPoint.Instructions;
+ if (instructions.Count == 3)
+ {
+ // stloc temp(isinst exceptionType(ldloc exceptionVar))
+ // if (comp(ldloc temp != ldnull)) br whenConditionBlock
+ // br falseBlock
+ context.Step($"Detected catch-when for {catchBlock.Variable.Name} (extra store)", instructions[0]);
+ ((StLoc)instructions[0]).Value = exceptionSlot;
+ instructions[1].ReplaceWith(new Branch(whenConditionBlock));
+ instructions.RemoveAt(2);
+ container.SortBlocks(deleteUnreachableBlocks: true);
+ }
+ else if (instructions.Count == 2)
+ {
+ // if (comp(isinst exceptionType(ldloc exceptionVar) != ldnull)) br whenConditionBlock
+ // br falseBlock
+ context.Step($"Detected catch-when for {catchBlock.Variable.Name}", instructions[0]);
+ var branch = new Branch(whenConditionBlock);
+ instructions[0].ReplaceWith(branch);
+ instructions.RemoveAt(1);
+ context.EndStep(branch);
+ container.SortBlocks(deleteUnreachableBlocks: true);
+ }
+
+ PropagateExceptionVariable(context, catchBlock);
+ }
+ }
+ }
+
+ ///
+ /// catch E_189 : 0200007C System.Exception when (BlockContainer {
+ /// Block IL_0079 (incoming: 1) {
+ /// stloc S_30(ldloc E_189)
+ /// br IL_0085
+ /// }
+ ///
+ /// Block IL_0085 (incoming: 1) {
+ /// stloc I_1(ldloc S_30)
+ /// where S_30 and I_1 are single definition
+ /// =>
+ /// copy-propagate E_189 to replace all uses of S_30 and I_1
+ ///
+ static void PropagateExceptionVariable(ILTransformContext context, TryCatchHandler handler)
+ {
+ var exceptionVariable = handler.Variable;
+ if (!exceptionVariable.IsSingleDefinition)
+ return;
+ context.StepStartGroup(nameof(PropagateExceptionVariable));
+ int i = 0;
+ while (i < exceptionVariable.LoadInstructions.Count)
+ {
+ var load = exceptionVariable.LoadInstructions[i];
+
+ if (!load.IsDescendantOf(handler))
+ {
+ i++;
+ continue;
+ }
+
+ // We are only interested in store "statements" copying the exception variable
+ // without modifying it.
+ var statement = Block.GetContainingStatement(load);
+ if (!(statement is StLoc stloc))
+ {
+ i++;
+ continue;
+ }
+ // simple copy case:
+ // stloc b(ldloc a)
+ if (stloc.Value == load)
+ {
+ PropagateExceptionInstance(stloc);
+ }
+ // if the type of the cast-class instruction matches the exceptionType,
+ // this cast can be removed without losing any side-effects.
+ // Note: this would also hold true iff exceptionType were an exception derived
+ // from cc.Type, however, we are more restrictive to match the pattern exactly.
+ // stloc b(castclass exceptionType(ldloc a))
+ else if (stloc.Value is CastClass cc && cc.Type.Equals(exceptionVariable.Type) && cc.Argument == load)
+ {
+ stloc.Value = load;
+ PropagateExceptionInstance(stloc);
+ }
+ else
+ {
+ i++;
+ }
+
+ void PropagateExceptionInstance(StLoc store)
+ {
+ foreach (var load in store.Variable.LoadInstructions.ToArray())
+ {
+ if (!load.IsDescendantOf(handler))
+ continue;
+ load.ReplaceWith(new LdLoc(exceptionVariable).WithILRange(load));
+ }
+ if (store.Variable.LoadCount == 0 && store.Parent is Block block)
+ {
+ block.Instructions.RemoveAt(store.ChildIndex);
+ }
+ else
+ {
+ i++;
+ }
+ }
+ }
+ context.StepEndGroup(keepIfEmpty: false);
+ }
+
+ ///
+ /// Block entryPoint (incoming: 1) {
+ /// stloc temp(isinst exceptionType(ldloc exceptionVar))
+ /// if (comp(ldloc temp != ldnull)) br whenConditionBlock
+ /// br falseBlock
+ /// }
+ ///
+ bool MatchCatchWhenEntryPoint(ILVariable exceptionVar, BlockContainer container, Block entryPoint, out IType exceptionType, out ILInstruction exceptionSlot, out Block whenConditionBlock)
+ {
+ exceptionType = null;
+ exceptionSlot = null;
+ whenConditionBlock = null;
+ if (entryPoint == null || entryPoint.IncomingEdgeCount != 1)
+ return false;
+ if (entryPoint.Instructions.Count == 3)
+ {
+ // stloc temp(isinst exceptionType(ldloc exceptionVar))
+ // if (comp(ldloc temp != ldnull)) br whenConditionBlock
+ // br falseBlock
+ if (!entryPoint.Instructions[0].MatchStLoc(out var temp, out var isinst) ||
+ temp.Kind != VariableKind.StackSlot || !isinst.MatchIsInst(out exceptionSlot, out exceptionType))
+ return false;
+ if (!exceptionSlot.MatchLdLoc(exceptionVar))
+ return false;
+ if (!entryPoint.Instructions[1].MatchIfInstruction(out var condition, out var branch))
+ return false;
+ if (!condition.MatchCompNotEquals(out var left, out var right))
+ return false;
+ if (!entryPoint.Instructions[2].MatchBranch(out var falseBlock) || !MatchFalseBlock(container, falseBlock, out var returnVar, out var exitBlock))
+ return false;
+ if ((left.MatchLdNull() && right.MatchLdLoc(temp)) || (right.MatchLdNull() && left.MatchLdLoc(temp)))
+ {
+ return branch.MatchBranch(out whenConditionBlock);
+ }
+ }
+ else if (entryPoint.Instructions.Count == 2)
+ {
+ // if (comp(isinst exceptionType(ldloc exceptionVar) != ldnull)) br whenConditionBlock
+ // br falseBlock
+ if (!entryPoint.Instructions[0].MatchIfInstruction(out var condition, out var branch))
+ return false;
+ if (!condition.MatchCompNotEquals(out var left, out var right))
+ return false;
+ if (!entryPoint.Instructions[1].MatchBranch(out var falseBlock) || !MatchFalseBlock(container, falseBlock, out var returnVar, out var exitBlock))
+ return false;
+ if (!left.MatchIsInst(out exceptionSlot, out exceptionType))
+ return false;
+ if (!exceptionSlot.MatchLdLoc(exceptionVar))
+ return false;
+ if (right.MatchLdNull())
+ {
+ return branch.MatchBranch(out whenConditionBlock);
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Block falseBlock (incoming: 1) {
+ /// stloc returnVar(ldc.i4 0)
+ /// br exitBlock
+ /// }
+ ///
+ bool MatchFalseBlock(BlockContainer container, Block falseBlock, out ILVariable returnVar, out Block exitBlock)
+ {
+ returnVar = null;
+ exitBlock = null;
+ if (falseBlock.IncomingEdgeCount != 1 || falseBlock.Instructions.Count != 2)
+ return false;
+ return falseBlock.Instructions[0].MatchStLoc(out returnVar, out var zero) &&
+ zero.MatchLdcI4(0) && falseBlock.Instructions[1].MatchBranch(out exitBlock) &&
+ MatchExitBlock(container, exitBlock, returnVar);
+ }
+
+ ///
+ /// Block exitBlock(incoming: 2) {
+ /// leave container(ldloc returnVar)
+ /// }
+ ///
+ bool MatchExitBlock(BlockContainer container, Block exitBlock, ILVariable returnVar)
+ {
+ if (exitBlock.IncomingEdgeCount != 2 || exitBlock.Instructions.Count != 1)
+ return false;
+ return exitBlock.Instructions[0].MatchLeave(container, out var value) &&
+ value.MatchLdLoc(returnVar);
+ }
+ }
+}
\ No newline at end of file