using System.Reflection;
using System.Reflection.Emit;
namespace AcDream.App.Tests.Architecture;
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
internal readonly record struct CompiledInstruction(int Offset, OpCode OpCode);
internal readonly record struct CompiledFieldReference(
int Offset,
OpCode OpCode,
FieldInfo Field);
internal readonly record struct CompiledBranch(
int Offset,
OpCode OpCode,
int TargetOffset);
///
/// 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.
///
internal static class CompiledCallGraph
{
private static readonly IReadOnlyDictionary 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 Read(MethodBase method)
=> ReadMethodReferences(method, includeDelegateTargets: false);
///
/// Reads calls, object construction, and method references used to build
/// delegates. The latter lets lifetime tests inspect an operation manifest
/// without invoking its real process/window resources.
///
public static IReadOnlyList ReadMethodReferences(MethodBase method) =>
ReadMethodReferences(method, includeDelegateTargets: true);
public static IReadOnlyList ReadDeclared(Type type)
{
ArgumentNullException.ThrowIfNull(type);
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
return type.GetMethods(flags)
.Cast()
.Concat(type.GetConstructors(flags))
.Where(method => method.GetMethodBody() is not null)
.SelectMany(Read)
.ToArray();
}
///
/// Reads a type's declared methods plus compiler-generated closure methods
/// nested beneath it. Composition tests use this when an owned callback or
/// acquisition factory carries the typed edge under review.
///
public static IReadOnlyList ReadOwned(Type type)
{
ArgumentNullException.ThrowIfNull(type);
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
MethodBase[] roots = EnumerateOwnedTypes(type)
.SelectMany(owner => owner.GetMethods(flags)
.Cast()
.Concat(owner.GetConstructors(flags)))
.Where(method => method.GetMethodBody() is not null)
.ToArray();
var pending = new Queue(roots);
var seen = new HashSet(roots);
var calls = new List();
while (pending.TryDequeue(out MethodBase? method))
{
foreach (CompiledCall call in ReadMethodReferences(method))
{
calls.Add(call);
if (call.Target.Module == type.Module
&& call.Target.Name.Contains('<', StringComparison.Ordinal)
&& call.Target.GetMethodBody() is not null
&& seen.Add(call.Target))
{
pending.Enqueue(call.Target);
}
}
}
return calls;
}
private static IEnumerable EnumerateOwnedTypes(Type root)
{
yield return root;
foreach (Type nested in root.GetNestedTypes(
BindingFlags.Public | BindingFlags.NonPublic))
{
foreach (Type owned in EnumerateOwnedTypes(nested))
yield return owned;
}
}
public static IReadOnlyList ReadStringLiterals(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
var literals = new List();
for (int cursor = 0; cursor < il.Length;)
{
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode == OpCodes.Ldstr)
{
int token = BitConverter.ToInt32(il, cursor);
literals.Add(method.Module.ResolveString(token));
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return literals;
}
///
/// Reads the compiled instruction sequence without exposing operands. This
/// is sufficient for architecture tests that need to distinguish an
/// immediate return from fall-through without freezing source formatting.
///
public static IReadOnlyList ReadInstructions(
MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
var instructions = new List();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
instructions.Add(new CompiledInstruction(instructionOffset, opCode));
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return instructions;
}
///
/// Reads compiled field loads and stores with their instruction offsets.
/// This supports lifetime/state-order checks whose observable contract is
/// a latch edge rather than a source spelling.
///
public static IReadOnlyList ReadFieldReferences(
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 references = new List();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.InlineField)
{
int token = BitConverter.ToInt32(il, cursor);
FieldInfo? field = method.Module.ResolveField(
token,
declaringArguments,
methodArguments);
if (field is not null)
{
references.Add(new CompiledFieldReference(
instructionOffset,
opCode,
field));
}
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return references;
}
///
/// Reads short and long branch destinations. Tests use this to prove that
/// an optional dependency guard jumps around a construction edge without
/// freezing the source expression that produced the branch.
///
public static IReadOnlyList ReadBranches(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
var branches = new List();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.ShortInlineBrTarget)
{
int target = cursor + sizeof(sbyte) + unchecked((sbyte)il[cursor]);
branches.Add(new CompiledBranch(instructionOffset, opCode, target));
}
else if (opCode.OperandType == OperandType.InlineBrTarget)
{
int target = cursor + sizeof(int) + BitConverter.ToInt32(il, cursor);
branches.Add(new CompiledBranch(instructionOffset, opCode, target));
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return branches;
}
///
/// Reads types named by compiled type operands such as casts, boxing, and
/// isinst. This lets tests retain an exact type boundary without
/// depending on the source expression used to spell it.
///
public static IReadOnlyList ReadTypeReferences(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 types = new List();
for (int cursor = 0; cursor < il.Length;)
{
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.InlineType)
{
int token = BitConverter.ToInt32(il, cursor);
Type? type = method.Module.ResolveType(
token,
declaringArguments,
methodArguments);
if (type is not null)
types.Add(type);
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return types;
}
public static int IndexOf(
IReadOnlyList 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 IReadOnlyList ReadMethodReferences(
MethodBase method,
bool includeDelegateTargets)
{
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();
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);
bool invocation = opCode == OpCodes.Call
|| opCode == OpCodes.Callvirt
|| opCode == OpCodes.Newobj;
bool delegateTarget = includeDelegateTargets
&& (opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn);
if (target is not null && (invocation || delegateTarget))
calls.Add(new CompiledCall(instructionOffset, target));
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return calls;
}
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."),
};
}