acdream/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs
2026-08-18 14:58:25 +02:00

109 lines
4.2 KiB
C#

using System.Reflection;
using System.Reflection.Emit;
namespace AcDream.App.Tests.Architecture;
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
/// <summary>
/// Reads compiled call/new-object edges from a method body. Architecture tests
/// use this when the contract is an ownership or ordering edge that cannot be
/// exercised through a public result, avoiding formatting- and comment-sensitive
/// source-string assertions.
/// </summary>
internal static class CompiledCallGraph
{
private static readonly IReadOnlyDictionary<short, OpCode> OpCodesByValue =
typeof(OpCodes)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(field => field.FieldType == typeof(OpCode))
.Select(field => (OpCode)field.GetValue(null)!)
.ToDictionary(opCode => opCode.Value);
public static IReadOnlyList<CompiledCall> Read(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
Type[]? declaringArguments = method.DeclaringType?.IsGenericType == true
? method.DeclaringType.GetGenericArguments()
: null;
Type[]? methodArguments = method.IsGenericMethod
? method.GetGenericArguments()
: null;
var calls = new List<CompiledCall>();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.InlineMethod)
{
int token = BitConverter.ToInt32(il, cursor);
MethodBase? target = method.Module.ResolveMethod(
token,
declaringArguments,
methodArguments);
if (target is not null && opCode is { Value: var value }
&& value is 0x28 or 0x6F or 0x73)
{
calls.Add(new CompiledCall(instructionOffset, target));
}
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return calls;
}
public static int IndexOf(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName,
int startIndex = 0) =>
Enumerable.Range(startIndex, calls.Count - startIndex)
.FirstOrDefault(
index => calls[index].Target.DeclaringType == declaringType
&& calls[index].Target.Name == methodName,
-1);
private static OpCode ReadOpCode(byte[] il, ref int cursor)
{
byte first = il[cursor++];
short value = first == 0xFE
? unchecked((short)(0xFE00 | il[cursor++]))
: first;
return OpCodesByValue.TryGetValue(value, out OpCode opCode)
? opCode
: throw new InvalidOperationException(
$"Unknown IL opcode 0x{unchecked((ushort)value):X4}.");
}
private static int OperandSize(OperandType operandType, byte[] il, int cursor) =>
operandType switch
{
OperandType.InlineNone => 0,
OperandType.ShortInlineBrTarget or
OperandType.ShortInlineI or
OperandType.ShortInlineVar => 1,
OperandType.InlineVar => 2,
OperandType.InlineBrTarget or
OperandType.InlineField or
OperandType.InlineI or
OperandType.InlineMethod or
OperandType.InlineSig or
OperandType.InlineString or
OperandType.InlineTok or
OperandType.InlineType or
OperandType.ShortInlineR => 4,
OperandType.InlineI8 or OperandType.InlineR => 8,
OperandType.InlineSwitch =>
sizeof(int) + (BitConverter.ToInt32(il, cursor) * sizeof(int)),
_ => throw new ArgumentOutOfRangeException(
nameof(operandType),
operandType,
"Unsupported IL operand type."),
};
}