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) { 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); 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 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."), }; }