-
-
Notifications
You must be signed in to change notification settings - Fork 65
Add basic support for Throw opcode #282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
595f464
e2666e3
9110f50
b499fb5
b2e0c95
b5d135b
a0441c7
a070fba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| using System; | ||
| using System.Linq.Expressions; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace DelegateDecompiler.Tests | ||
| { | ||
| [TestFixture] | ||
| public class ThrowIntegrationTests : DecompilerTestsBase | ||
| { | ||
| [Test] | ||
| public void Should_decompile_simple_throw() | ||
| { | ||
| Action<int> method = x => { if (x < 0) throw new ArgumentException("negative"); }; | ||
|
|
||
| // Get the decompiled expression | ||
| var decompiled = method.Decompile(); | ||
|
|
||
| // Verify it contains a throw expression | ||
| var decompiledString = decompiled.Body.ToString(); | ||
| Console.WriteLine($"Decompiled: {decompiledString}"); | ||
|
|
||
| // The decompiled expression should contain the throw | ||
| Assert.That(decompiledString, Does.Contain("throw")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_decompile_throw_expression() | ||
| { | ||
| // Test with a simpler function that directly throws | ||
| Action method = () => throw new ArgumentException("test"); | ||
|
|
||
| // Get the decompiled expression | ||
| var decompiled = method.Decompile(); | ||
|
|
||
| // Verify it contains a throw expression | ||
| var decompiledString = decompiled.Body.ToString(); | ||
| Console.WriteLine($"Decompiled: {decompiledString}"); | ||
|
|
||
| // The decompiled expression should contain the throw | ||
| Assert.That(decompiledString, Does.Contain("throw")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_decompile_simple_rethrow() | ||
| { | ||
| Action method = () => | ||
| { | ||
| try | ||
|
||
| { | ||
| throw new ArgumentException("original"); | ||
| } | ||
| catch (ArgumentException) | ||
| { | ||
| throw; // This should generate a rethrow opcode | ||
| } | ||
| }; | ||
|
|
||
| // Get the decompiled expression | ||
| var decompiled = method.Decompile(); | ||
|
|
||
| // Verify it contains expressions (even if rethrow isn't directly visible in string representation) | ||
| var decompiledString = decompiled.Body.ToString(); | ||
| Console.WriteLine($"Decompiled: {decompiledString}"); | ||
|
|
||
| // The method should decompile without throwing an exception | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| Assert.That(decompiled.Body, Is.Not.Null); | ||
| } | ||
|
|
||
| // Test method with computed attribute that includes throw | ||
| public class TestClass | ||
| { | ||
| public int Value { get; set; } | ||
|
|
||
| [Computed] | ||
| public int PositiveValue => Value > 0 ? Value : throw new ArgumentException("Value must be positive"); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_decompile_computed_property_with_throw() | ||
| { | ||
| // This should work now that throw is supported | ||
| var propertyInfo = typeof(TestClass).GetProperty(nameof(TestClass.PositiveValue)); | ||
| var getterMethod = propertyInfo.GetGetMethod(); | ||
|
|
||
| var decompiled = getterMethod.Decompile(); | ||
| Console.WriteLine($"Decompiled computed property: {decompiled.Body}"); | ||
|
||
|
|
||
| // Should decompile without exception | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| Assert.That(decompiled.Body, Is.Not.Null); | ||
|
|
||
| // Check if the expression contains throw operations | ||
| var visitor = new ThrowExpressionVisitor(); | ||
| visitor.Visit(decompiled.Body); | ||
| Assert.That(visitor.HasThrowExpression, Is.True, "Should contain throw expressions"); | ||
| } | ||
| } | ||
|
|
||
| // Visitor to check for throw expressions in the expression tree | ||
| public class ThrowExpressionVisitor : ExpressionVisitor | ||
| { | ||
| public bool HasThrowExpression { get; private set; } | ||
|
|
||
| public override Expression Visit(Expression node) | ||
| { | ||
| if (node != null && node.NodeType == ExpressionType.Throw) | ||
| { | ||
| HasThrowExpression = true; | ||
| } | ||
| return base.Visit(node); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| using System; | ||
| using System.Linq.Expressions; | ||
| using System.Reflection.Emit; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace DelegateDecompiler.Tests | ||
| { | ||
| [TestFixture] | ||
| public class ThrowOpcodeValidationTests : DecompilerTestsBase | ||
|
||
| { | ||
| [Test] | ||
| public void OpCodes_Throw_Is_Now_Supported() | ||
| { | ||
| // Before the fix, this would have thrown a NotSupportedException about OpCodes.Throw being unsupported | ||
| // Now it should work correctly | ||
| Action method = () => throw new InvalidOperationException("Test exception"); | ||
|
|
||
| Assert.DoesNotThrow(() => { | ||
| var decompiled = method.Decompile(); | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| Assert.That(decompiled.Body, Is.Not.Null); | ||
| Assert.That(decompiled.Body.NodeType, Is.EqualTo(ExpressionType.Throw)); | ||
| }); | ||
| } | ||
|
|
||
| [Test] | ||
| public void OpCodes_Rethrow_Is_Now_Supported() | ||
| { | ||
| // Before the fix, this would have thrown a NotSupportedException about OpCodes.Rethrow being unsupported | ||
| // Now it should work correctly | ||
| Action method = () => | ||
| { | ||
| try | ||
| { | ||
| throw new InvalidOperationException("Original exception"); | ||
| } | ||
| catch (InvalidOperationException) | ||
| { | ||
| throw; // This generates OpCodes.Rethrow | ||
| } | ||
| }; | ||
|
|
||
| Assert.DoesNotThrow(() => { | ||
| var decompiled = method.Decompile(); | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| Assert.That(decompiled.Body, Is.Not.Null); | ||
| }); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Conditional_Expression_With_Throw_Is_Supported() | ||
| { | ||
| // This tests the fixed stack merge logic for conditional branches with throw | ||
| Func<int, string> method = x => x > 0 ? "positive" : throw new ArgumentException("negative"); | ||
|
|
||
| Assert.DoesNotThrow(() => { | ||
| var decompiled = method.Decompile(); | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| Assert.That(decompiled.Body, Is.Not.Null); | ||
|
|
||
| // The decompiled expression should contain both a conditional and throw | ||
| var visitor = new ThrowExpressionVisitor(); | ||
| visitor.Visit(decompiled.Body); | ||
| Assert.That(visitor.HasThrowExpression, Is.True, "Should contain throw expressions"); | ||
| }); | ||
| } | ||
|
|
||
| [Test] | ||
| [TestCase("Should handle simple throw")] | ||
| [TestCase("Should handle throw in conditional")] | ||
| [TestCase("Should handle rethrow")] | ||
| public void ThrowProcessor_Integration_Test(string scenario) | ||
| { | ||
| Action testAction = scenario switch | ||
| { | ||
| "Should handle simple throw" => () => throw new Exception("test"), | ||
| "Should handle throw in conditional" => () => { if (true) throw new Exception("conditional"); }, | ||
| "Should handle rethrow" => () => { try { throw new Exception("original"); } catch { throw; } }, | ||
| _ => throw new ArgumentException($"Unknown scenario: {scenario}") | ||
| }; | ||
|
|
||
| // None of these should throw a NotSupportedException anymore | ||
| Assert.DoesNotThrow(() => { | ||
| var decompiled = testAction.Decompile(); | ||
| Assert.That(decompiled, Is.Not.Null); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq.Expressions; | ||
| using System.Reflection; | ||
| using System.Reflection.Emit; | ||
| using DelegateDecompiler.Processors; | ||
| using NUnit.Framework; | ||
| using Mono.Reflection; | ||
|
|
||
| namespace DelegateDecompiler.Tests | ||
| { | ||
| [TestFixture] | ||
| public class ThrowProcessorTests : DecompilerTestsBase | ||
hazzik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| static readonly ConstructorInfo InstructionCtor = typeof(Instruction) | ||
| .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(int), typeof(OpCode) }, null); | ||
|
|
||
| [Test] | ||
| public void ThrowProcessor_HandlesThrowOpCode() | ||
| { | ||
| var instruction = CreateInstruction(0, OpCodes.Throw); | ||
| var stack = new Stack<Address>(); | ||
|
|
||
| // Push an exception expression onto the stack | ||
| var exceptionExpression = Expression.Constant(new ArgumentException("test")); | ||
| stack.Push(exceptionExpression); // Implicit conversion to Address | ||
|
|
||
| var state = new ProcessorState(true, stack, new VariableInfo[0], new List<Address>(), instruction); | ||
| var processor = new ThrowProcessor(); | ||
|
|
||
| var result = processor.Process(state); | ||
|
|
||
| Assert.That(result, Is.True); | ||
| Assert.That(state.Stack.Count, Is.EqualTo(1)); | ||
|
|
||
| var throwExpression = state.Stack.Pop().Expression; | ||
| Assert.That(throwExpression.NodeType, Is.EqualTo(ExpressionType.Throw)); | ||
| Assert.That(throwExpression, Is.TypeOf<UnaryExpression>()); | ||
|
|
||
| var unaryExpression = (UnaryExpression)throwExpression; | ||
| Assert.That(unaryExpression.Operand, Is.EqualTo(exceptionExpression)); | ||
| Assert.That(unaryExpression.Type, Is.EqualTo(typeof(void))); // Throw returns void | ||
| } | ||
|
|
||
| [Test] | ||
| public void ThrowProcessor_HandlesRethrowOpCode() | ||
| { | ||
| var instruction = CreateInstruction(0, OpCodes.Rethrow); | ||
| var stack = new Stack<Address>(); | ||
|
|
||
| var state = new ProcessorState(true, stack, new VariableInfo[0], new List<Address>(), instruction); | ||
| var processor = new ThrowProcessor(); | ||
|
|
||
| var result = processor.Process(state); | ||
|
|
||
| Assert.That(result, Is.True); | ||
| Assert.That(state.Stack.Count, Is.EqualTo(1)); | ||
|
|
||
| var rethrowExpression = state.Stack.Pop().Expression; | ||
| Assert.That(rethrowExpression.NodeType, Is.EqualTo(ExpressionType.Throw)); | ||
| Assert.That(rethrowExpression, Is.TypeOf<UnaryExpression>()); | ||
|
|
||
| var unaryExpression = (UnaryExpression)rethrowExpression; | ||
| Assert.That(unaryExpression.Operand, Is.Null); // Rethrow has no operand | ||
| Assert.That(unaryExpression.Type, Is.EqualTo(typeof(void))); // Rethrow returns void | ||
| } | ||
|
|
||
| [Test] | ||
| public void ThrowProcessor_IgnoresOtherOpCodes() | ||
| { | ||
| var instruction = CreateInstruction(0, OpCodes.Add); | ||
| var state = new ProcessorState(true, new Stack<Address>(), new VariableInfo[0], new List<Address>(), instruction); | ||
| var processor = new ThrowProcessor(); | ||
|
|
||
| var result = processor.Process(state); | ||
|
|
||
| Assert.That(result, Is.False); | ||
| } | ||
|
|
||
| static Instruction CreateInstruction(int offset, OpCode opcode) | ||
| { | ||
| var instruction = (Instruction)InstructionCtor.Invoke(new object[] { offset, opcode }); | ||
| Assume.That(instruction, Is.Not.Null); | ||
| return instruction; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| using System.Linq.Expressions; | ||
| using System.Reflection.Emit; | ||
|
|
||
| namespace DelegateDecompiler.Processors; | ||
|
|
||
| internal class ThrowProcessor : IProcessor | ||
| { | ||
| public bool Process(ProcessorState state) | ||
| { | ||
| if (state.Instruction.OpCode == OpCodes.Throw) | ||
| { | ||
| // OpCodes.Throw pops the exception from the stack and throws it | ||
hazzik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var exception = state.Stack.Pop(); | ||
| var throwExpression = Expression.Throw(exception, typeof(void)); | ||
hazzik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| state.Stack.Push(throwExpression); | ||
| return true; | ||
| } | ||
|
|
||
| if (state.Instruction.OpCode == OpCodes.Rethrow) | ||
| { | ||
| // OpCodes.Rethrow doesn't pop anything from the stack and rethrows the current exception | ||
| var rethrowExpression = Expression.Rethrow(typeof(void)); | ||
| state.Stack.Push(rethrowExpression); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.