Large types (e.g., 15.5M chars of IL from CyBGElaborator) crashed MCP clients. Two fixes: 1. decompile_assembly now truncates output exceeding max_output_chars (default 100K), saves the full output to a temp file, and returns a preview with recovery instructions. Set max_output_chars=0 to disable. 2. New decompile_method tool extracts individual methods from a type. IL mode uses ECMA-335 .method/end-of-method delimiters. C# mode uses signature matching with brace-depth counting. Handles overloads automatically. New module: il_parser.py with extract_il_method() and extract_csharp_method() functions, keeping parsing logic separate from server.py.
232 lines
7.3 KiB
Python
232 lines
7.3 KiB
Python
"""Tests for IL and C# method extraction from decompiled output."""
|
|
|
|
from mcilspy.il_parser import extract_csharp_method, extract_il_method
|
|
|
|
# ── IL extraction fixtures ────────────────────────────────────────────
|
|
|
|
|
|
SAMPLE_IL = """\
|
|
.class public auto ansi beforefieldinit MyNamespace.MyClass
|
|
extends [mscorlib]System.Object
|
|
{
|
|
.method public hidebysig
|
|
instance void DoElaborate (
|
|
int32 x
|
|
) cil managed
|
|
{
|
|
.maxstack 8
|
|
IL_0000: nop
|
|
IL_0001: ldarg.1
|
|
IL_0002: call instance void MyNamespace.MyClass::Helper(int32)
|
|
IL_0007: nop
|
|
IL_0008: ret
|
|
} // end of method MyClass::DoElaborate
|
|
|
|
.method public hidebysig
|
|
instance void DoElaborate (
|
|
int32 x,
|
|
string y
|
|
) cil managed
|
|
{
|
|
.maxstack 8
|
|
IL_0000: nop
|
|
IL_0001: ret
|
|
} // end of method MyClass::DoElaborate
|
|
|
|
.method private hidebysig
|
|
instance void Helper (
|
|
int32 val
|
|
) cil managed
|
|
{
|
|
.maxstack 8
|
|
IL_0000: nop
|
|
IL_0001: ret
|
|
} // end of method MyClass::Helper
|
|
|
|
} // end of class MyNamespace.MyClass
|
|
"""
|
|
|
|
|
|
SAMPLE_CSHARP = """\
|
|
namespace MyNamespace
|
|
{
|
|
public class MyClass
|
|
{
|
|
public void DoElaborate(int x)
|
|
{
|
|
Helper(x);
|
|
}
|
|
|
|
public void DoElaborate(int x, string y)
|
|
{
|
|
Console.WriteLine(y);
|
|
}
|
|
|
|
private void Helper(int val)
|
|
{
|
|
// internal helper
|
|
}
|
|
|
|
public static async Task<bool> ProcessAsync(
|
|
string input,
|
|
CancellationToken ct)
|
|
{
|
|
await Task.Delay(100, ct);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
|
|
# ── IL extraction tests ──────────────────────────────────────────────
|
|
|
|
|
|
class TestExtractILMethod:
|
|
"""Tests for extract_il_method()."""
|
|
|
|
def test_extracts_single_method(self):
|
|
"""Should extract the Helper method."""
|
|
results = extract_il_method(SAMPLE_IL, "Helper")
|
|
assert len(results) == 1
|
|
assert ".method private hidebysig" in results[0]
|
|
assert "end of method MyClass::Helper" in results[0]
|
|
|
|
def test_extracts_overloaded_methods(self):
|
|
"""Should extract both DoElaborate overloads."""
|
|
results = extract_il_method(SAMPLE_IL, "DoElaborate")
|
|
assert len(results) == 2
|
|
# First overload has one param
|
|
assert "int32 x" in results[0]
|
|
# Second overload has two params
|
|
assert "string y" in results[1]
|
|
|
|
def test_returns_empty_for_nonexistent_method(self):
|
|
"""Should return empty list when method doesn't exist."""
|
|
results = extract_il_method(SAMPLE_IL, "NonExistent")
|
|
assert results == []
|
|
|
|
def test_returns_empty_for_empty_input(self):
|
|
"""Should handle empty inputs gracefully."""
|
|
assert extract_il_method("", "Foo") == []
|
|
assert extract_il_method(SAMPLE_IL, "") == []
|
|
assert extract_il_method("", "") == []
|
|
|
|
def test_method_name_is_exact_match(self):
|
|
"""Should not match partial method names."""
|
|
results = extract_il_method(SAMPLE_IL, "Do")
|
|
assert results == []
|
|
|
|
def test_method_name_is_exact_match_helper(self):
|
|
"""Should not match 'Help' when looking for 'Helper'."""
|
|
results = extract_il_method(SAMPLE_IL, "Help")
|
|
assert results == []
|
|
|
|
def test_preserves_method_body(self):
|
|
"""Extracted method should include the full body."""
|
|
results = extract_il_method(SAMPLE_IL, "Helper")
|
|
assert len(results) == 1
|
|
assert ".maxstack 8" in results[0]
|
|
assert "IL_0000: nop" in results[0]
|
|
assert "IL_0001: ret" in results[0]
|
|
|
|
|
|
# ── C# extraction tests ──────────────────────────────────────────────
|
|
|
|
|
|
class TestExtractCSharpMethod:
|
|
"""Tests for extract_csharp_method()."""
|
|
|
|
def test_extracts_single_method(self):
|
|
"""Should extract the Helper method."""
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "Helper")
|
|
assert len(results) == 1
|
|
assert "private void Helper" in results[0]
|
|
assert "// internal helper" in results[0]
|
|
|
|
def test_extracts_overloaded_methods(self):
|
|
"""Should extract both DoElaborate overloads."""
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "DoElaborate")
|
|
assert len(results) == 2
|
|
|
|
def test_returns_empty_for_nonexistent_method(self):
|
|
"""Should return empty list when method doesn't exist."""
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "NonExistent")
|
|
assert results == []
|
|
|
|
def test_returns_empty_for_empty_input(self):
|
|
"""Should handle empty inputs gracefully."""
|
|
assert extract_csharp_method("", "Foo") == []
|
|
assert extract_csharp_method(SAMPLE_CSHARP, "") == []
|
|
|
|
def test_extracts_multiline_signature(self):
|
|
"""Should handle method signatures that span multiple lines."""
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "ProcessAsync")
|
|
assert len(results) == 1
|
|
assert "async Task<bool>" in results[0]
|
|
assert "CancellationToken ct" in results[0]
|
|
assert "return true;" in results[0]
|
|
|
|
def test_preserves_method_body(self):
|
|
"""Extracted method should include the full body."""
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "Helper")
|
|
assert len(results) == 1
|
|
assert "// internal helper" in results[0]
|
|
|
|
def test_method_name_is_exact_match(self):
|
|
"""Should not match partial method names via regex anchoring."""
|
|
# "Do" should not match "DoElaborate" because the pattern requires
|
|
# the name followed by optional generics and opening paren
|
|
results = extract_csharp_method(SAMPLE_CSHARP, "Do")
|
|
assert results == []
|
|
|
|
|
|
# ── Edge case tests ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Tests for edge cases in both extractors."""
|
|
|
|
def test_il_method_with_special_chars_in_name(self):
|
|
"""Methods with dots or special chars in type name should work."""
|
|
il = """\
|
|
.method public hidebysig
|
|
instance void Process () cil managed
|
|
{
|
|
.maxstack 8
|
|
IL_0000: ret
|
|
} // end of method Outer+Nested::Process
|
|
"""
|
|
results = extract_il_method(il, "Process")
|
|
assert len(results) == 1
|
|
|
|
def test_csharp_method_with_generic_return(self):
|
|
"""Should handle generic return types."""
|
|
cs = """\
|
|
public class Foo
|
|
{
|
|
public List<Dictionary<string, int>> Transform(IEnumerable<string> input)
|
|
{
|
|
return new();
|
|
}
|
|
}
|
|
"""
|
|
results = extract_csharp_method(cs, "Transform")
|
|
assert len(results) == 1
|
|
assert "return new();" in results[0]
|
|
|
|
def test_csharp_method_expression_body(self):
|
|
"""Expression-bodied methods should be extracted (they use => not {})."""
|
|
cs = """\
|
|
public class Foo
|
|
{
|
|
public int GetValue()
|
|
{
|
|
return 42;
|
|
}
|
|
}
|
|
"""
|
|
results = extract_csharp_method(cs, "GetValue")
|
|
assert len(results) == 1
|
|
assert "return 42;" in results[0]
|