mcilspy/tests/test_il_parser.py
Ryan Malloy f15905b350 feat: add output truncation guard and method-level decompilation
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.
2026-03-02 17:19:42 -07:00

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]