using System.Reflection; using System.Reflection.Emit; namespace AcDream.App.Tests.Architecture; internal readonly record struct CompiledCall(int Offset, MethodBase Target); /// /// 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(); } 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; } 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."), }; }