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