228 lines
8.9 KiB
C#
228 lines
8.9 KiB
C#
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);
|
|
|
|
/// <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)
|
|
=> ReadMethodReferences(method, includeDelegateTargets: false);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static IReadOnlyList<CompiledCall> ReadMethodReferences(MethodBase method) =>
|
|
ReadMethodReferences(method, includeDelegateTargets: true);
|
|
|
|
public static IReadOnlyList<CompiledCall> ReadDeclared(Type type)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(type);
|
|
const BindingFlags flags = BindingFlags.Instance
|
|
| BindingFlags.Static
|
|
| BindingFlags.Public
|
|
| BindingFlags.NonPublic
|
|
| BindingFlags.DeclaredOnly;
|
|
return type.GetMethods(flags)
|
|
.Cast<MethodBase>()
|
|
.Concat(type.GetConstructors(flags))
|
|
.Where(method => method.GetMethodBody() is not null)
|
|
.SelectMany(Read)
|
|
.ToArray();
|
|
}
|
|
|
|
public static IReadOnlyList<string> 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<string>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static IReadOnlyList<CompiledInstruction> 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<CompiledInstruction>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads types named by compiled type operands such as casts, boxing, and
|
|
/// <c>isinst</c>. This lets tests retain an exact type boundary without
|
|
/// depending on the source expression used to spell it.
|
|
/// </summary>
|
|
public static IReadOnlyList<Type> 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<Type>();
|
|
|
|
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<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 IReadOnlyList<CompiledCall> 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<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);
|
|
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."),
|
|
};
|
|
}
|