diff --git a/Ben.Demystifier.sln b/Ben.Demystifier.sln new file mode 100644 index 0000000..dbecaf3 --- /dev/null +++ b/Ben.Demystifier.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27019.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{59CA6310-4AA5-4093-95D4-472B94DC0CD4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier", "src\Ben.Demystifier\Ben.Demystifier.csproj", "{5410A056-89AB-4912-BD1E-A63616AD91D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Test", "test\Ben.Demystifier.Test\Ben.Demystifier.Test.csproj", "{B9E150B0-AEEB-4D98-8BE1-92C1296699A2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{455921D3-DD54-4355-85CF-F4009DF2AB70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackTrace", "sample\StackTrace\StackTrace.csproj", "{E161FC12-53C2-47CD-A5FC-3684B86723A9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.Build.0 = Release|Any CPU + {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5410A056-89AB-4912-BD1E-A63616AD91D0} = {A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3} + {B9E150B0-AEEB-4D98-8BE1-92C1296699A2} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4} + {E161FC12-53C2-47CD-A5FC-3684B86723A9} = {455921D3-DD54-4355-85CF-F4009DF2AB70} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {841B7D5F-E810-4F94-A529-002C7E075216} + EndGlobalSection +EndGlobal diff --git a/sample/StackTrace/Program.cs b/sample/StackTrace/Program.cs new file mode 100644 index 0000000..dda43af --- /dev/null +++ b/sample/StackTrace/Program.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +class Program +{ + static void Main(string[] args) + { + Exception exception = null; + try + { + new Program(); + } + catch (Exception ex) + { + exception = ex.Demystify(); + } + + Console.WriteLine(exception); + } + + static Action s_action = (string s, bool b) => s_func(s, b); + static Func s_func = (string s, bool b) => (RefMethod(s), b); + + Action, object> _action = (Action lambda, object state) => lambda(state); + + static string s = ""; + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + Program() : this(() => Start()) + { + + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + Program(Action action) + { + RunAction((state) => _action((s) => action(), state), null); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static IEnumerable Iterator(int startAt) + { + var list = new List() { 1, 2, 3, 4 }; + foreach (var item in list) + { + // Throws the exception + list.Add(item); + + yield return item.ToString(); + } + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static async Task MethodAsync(int value) + { + await Task.Delay(0); + return GenericClass.GenericMethod(ref value); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static async Task MethodAsync(TValue value) + { + return await MethodAsync(1); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static void RunAction(Action lambda, object state) + { + lambda(state); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static string RunLambda(Func lambda) + { + + return lambda(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static (string val, bool) Method(string value) + { + Func func = () => MethodAsync(value).GetAwaiter().GetResult(); + var anonType = new { func }; + return (RunLambda(() => anonType.func()), true); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static ref string RefMethod(int value) + { + return ref s; + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static string RefMethod(in string value) + { + var val = value; + return LocalFuncParam(value).ToString(); + + int LocalFuncParam(string s) + { + return int.Parse(LocalFuncRefReturn()); + } + + ref string LocalFuncRefReturn() + { + Method(val); + return ref s; + } + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static string Start() + { + return LocalFunc2(true, false).ToString(); + + void LocalFunc1(long l) + { + Start((val: "", true)); + } + + bool LocalFunc2(bool b1, bool b2) + { + LocalFunc1(1); + return true; + } + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static ref string RefMethod(bool value) + { + return ref s; + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static void Start((string val, bool) param) + { + s_action.Invoke(param.val, param.Item2); + } + + + class GenericClass + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static string GenericMethod(ref TSubType value) + { + var returnVal = ""; + for (var i = 0; i < 10; i++) + { + try + { + returnVal += string.Join(", ", Iterator(5).Select(s => s)); + } + catch (Exception ex) + { + throw new Exception(ex.Message, ex); + } + } + + return returnVal; + } + } +} diff --git a/sample/StackTrace/StackTrace.csproj b/sample/StackTrace/StackTrace.csproj new file mode 100644 index 0000000..c74a7c5 --- /dev/null +++ b/sample/StackTrace/StackTrace.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp2.0 + + + + 7.2 + + + + 7.2 + + + + + + + diff --git a/src/Ben.Demystifier/Ben.Demystifier.csproj b/src/Ben.Demystifier/Ben.Demystifier.csproj new file mode 100644 index 0000000..55f39ee --- /dev/null +++ b/src/Ben.Demystifier/Ben.Demystifier.csproj @@ -0,0 +1,35 @@ + + + + Ben Core + Ben.Demystifier + High performance understanding for stack traces (Make error logs more productive) + ben_a_adams + https://github.com/benaadams/Ben.Demystifier + git + true + true + 0.0.1 + + + + netstandard2.0;net46 + 7.2 + + + + + 2.4.0 + + + 4.4.1 + + + 1.5.0 + + + 4.4.0 + + + + diff --git a/src/Ben.Demystifier/EnhancedStackFrame.cs b/src/Ben.Demystifier/EnhancedStackFrame.cs new file mode 100644 index 0000000..9e8a064 --- /dev/null +++ b/src/Ben.Demystifier/EnhancedStackFrame.cs @@ -0,0 +1,79 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +namespace System.Diagnostics +{ + public class EnhancedStackFrame : StackFrame + { + private string _fileName; + private int _lineNumber; + private int _colNumber; + + public StackFrame StackFrame { get; } + + public ResolvedMethod MethodInfo { get; } + + internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string fileName, int lineNumber, int colNumber) + : base(fileName, lineNumber, colNumber) + { + StackFrame = stackFrame; + MethodInfo = methodInfo; + + _fileName = fileName; + _lineNumber = lineNumber; + _colNumber = colNumber; + } + + /// + /// Gets the column number in the file that contains the code that is executing. + /// This information is typically extracted from the debugging symbols for the executable. + /// + /// The file column number, or 0 (zero) if the file column number cannot be determined. + public override int GetFileColumnNumber() => _colNumber; + + /// + /// Gets the line number in the file that contains the code that is executing. + /// This information is typically extracted from the debugging symbols for the executable. + /// + /// The file line number, or 0 (zero) if the file line number cannot be determined. + public override int GetFileLineNumber() => _lineNumber; + + /// + /// Gets the file name that contains the code that is executing. + /// This information is typically extracted from the debugging symbols for the executable. + /// + /// The file name, or null if the file name cannot be determined. + public override string GetFileName() => _fileName; + + /// + /// Gets the offset from the start of the Microsoft intermediate language (MSIL) + /// code for the method that is executing. This offset might be an approximation + /// depending on whether or not the just-in-time (JIT) compiler is generating debugging + /// code. The generation of this debugging information is controlled by the System.Diagnostics.DebuggableAttribute. + /// + /// The offset from the start of the MSIL code for the method that is executing. + public override int GetILOffset() => StackFrame.GetILOffset(); + + /// + /// Gets the method in which the frame is executing. + /// + /// The method in which the frame is executing. + public override MethodBase GetMethod() => StackFrame.GetMethod(); + + /// + /// Gets the offset from the start of the native just-in-time (JIT)-compiled code + /// for the method that is being executed. The generation of this debugging information + /// is controlled by the System.Diagnostics.DebuggableAttribute class. + /// + /// The offset from the start of the JIT-compiled code for the method that is being executed. + public override int GetNativeOffset() => StackFrame.GetNativeOffset(); + + /// + /// Builds a readable representation of the stack trace. + /// + /// A readable representation of the stack trace. + public override string ToString() => MethodInfo.ToString(); + } +} diff --git a/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs new file mode 100644 index 0000000..65ba4df --- /dev/null +++ b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs @@ -0,0 +1,698 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Generic.Enumerable; +using System.Diagnostics.Internal; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Text; + +namespace System.Diagnostics +{ + public partial class EnhancedStackTrace + { + private static List GetFrames(Exception exception) + { + var frames = new List(); + if (exception == null) + { + return frames; + } + + using (var portablePdbReader = new PortablePdbReader()) + { + var needFileInfo = true; + var stackTrace = new StackTrace(exception, needFileInfo); + var stackFrames = stackTrace.GetFrames(); + + if (stackFrames == null) + { + return default; + } + + for (var i = 0; i < stackFrames.Length; i++) + { + var frame = stackFrames[i]; + var method = frame.GetMethod(); + + // Always show last stackFrame + if (!ShowInStackTrace(method) && i < stackFrames.Length - 1) + { + continue; + } + + var fileName = frame.GetFileName(); + var row = frame.GetFileLineNumber(); + var column = frame.GetFileColumnNumber(); + + if (string.IsNullOrEmpty(fileName)) + { + // .NET Framework and older versions of mono don't support portable PDBs + // so we read it manually to get file name and line information + portablePdbReader.PopulateStackFrame(frame, method, frame.GetILOffset(), out fileName, out row, out column); + } + + var stackFrame = new EnhancedStackFrame(frame, GetMethodDisplayString(method), fileName, row, column); + + + frames.Add(stackFrame); + } + + return frames; + } + } + + private static ResolvedMethod GetMethodDisplayString(MethodBase originMethod) + { + // Special case: no method available + if (originMethod == null) + { + return null; + } + + MethodBase method = originMethod; + + var methodDisplayInfo = new ResolvedMethod(); + methodDisplayInfo.SubMethodBase = method; + + // Type name + var type = method.DeclaringType; + + var subMethodName = method.Name; + var methodName = method.Name; + + if (type != null && type.IsDefined(typeof(CompilerGeneratedAttribute)) && + (typeof(IAsyncStateMachine).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type))) + { + methodDisplayInfo.IsAsync = typeof(IAsyncStateMachine).IsAssignableFrom(type); + + // Convert StateMachine methods to correct overload +MoveNext() + if (!TryResolveStateMachineMethod(ref method, out type)) + { + methodDisplayInfo.SubMethodBase = null; + subMethodName = null; + } + + methodName = method.Name; + } + + // Method name + methodDisplayInfo.MethodBase = method; + methodDisplayInfo.Name = methodName; + if (method.Name.IndexOf("<") >= 0) + { + if (TryResolveGeneratedName(ref method, out type, out methodName, out subMethodName, out var kind, out var ordinal)) + { + methodName = method.Name; + methodDisplayInfo.MethodBase = method; + methodDisplayInfo.Name = methodName; + methodDisplayInfo.Ordinal = ordinal; + } + else + { + methodDisplayInfo.MethodBase = null; + } + + methodDisplayInfo.IsLambda = (kind == GeneratedNameKind.LambdaMethod); + + if (methodDisplayInfo.IsLambda && type != null) + { + if (methodName == ".cctor") + { + var fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + foreach (var field in fields) + { + var value = field.GetValue(field); + if (value is Delegate d) + { + if (ReferenceEquals(d.Method, originMethod) && + d.Target.ToString() == originMethod.DeclaringType.ToString()) + { + methodDisplayInfo.Name = field.Name; + methodDisplayInfo.IsLambda = false; + method = originMethod; + break; + } + } + } + } + } + } + + if (subMethodName != methodName) + { + methodDisplayInfo.SubMethod = subMethodName; + } + + // ResolveStateMachineMethod may have set declaringType to null + if (type != null) + { + var declaringTypeName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, includeGenericParameterNames: true); + methodDisplayInfo.DeclaringTypeName = declaringTypeName; + } + + if (method is System.Reflection.MethodInfo mi) + { + methodDisplayInfo.ReturnParameter = GetParameter(mi.ReturnParameter); + } + + if (method.IsGenericMethod) + { + var genericArguments = string.Join(", ", method.GetGenericArguments() + .Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true))); + methodDisplayInfo.GenericArguments += "<" + genericArguments + ">"; + } + + // Method parameters + var parameters = method.GetParameters(); + if (parameters.Length > 0) + { + var parameterList = new List(parameters.Length); + foreach (var parameter in parameters) + { + parameterList.Add(GetParameter(parameter)); + } + + methodDisplayInfo.Parameters = parameterList; + } + + if (methodDisplayInfo.SubMethodBase == methodDisplayInfo.MethodBase) + { + methodDisplayInfo.SubMethodBase = null; + } + else if (methodDisplayInfo.SubMethodBase != null) + { + parameters = methodDisplayInfo.SubMethodBase.GetParameters(); + if (parameters.Length > 0) + { + var parameterList = new List(parameters.Length); + foreach (var parameter in parameters) + { + var param = GetParameter(parameter); + if (param.Name?.StartsWith("<") ?? true) continue; + + parameterList.Add(param); + } + + methodDisplayInfo.SubMethodParameters = parameterList; + } + } + + return methodDisplayInfo; + } + + private static bool TryResolveGeneratedName(ref MethodBase method, out Type type, out string methodName, out string subMethodName, out GeneratedNameKind kind, out int? ordinal) + { + kind = GeneratedNameKind.None; + type = method.DeclaringType; + subMethodName = null; + ordinal = null; + methodName = method.Name; + + var generatedName = methodName; + + if (!TryParseGeneratedName(generatedName, out kind, out int openBracketOffset, out int closeBracketOffset)) + { + return false; + } + + methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1); + + switch (kind) + { + case GeneratedNameKind.LocalFunction: + { + var localNameStart = generatedName.IndexOf((char)kind, closeBracketOffset + 1); + if (localNameStart < 0) break; + localNameStart += 3; + + if (localNameStart < generatedName.Length) + { + var localNameEnd = generatedName.IndexOf("|", localNameStart); + if (localNameEnd > 0) + { + subMethodName = generatedName.Substring(localNameStart, localNameEnd - localNameStart); + } + } + break; + } + case GeneratedNameKind.LambdaMethod: + subMethodName = ""; + break; + } + + var dt = method.DeclaringType; + if (dt == null) + { + return false; + } + + var matchHint = GetMatchHint(kind, method); + + var matchName = methodName; + + var candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); + if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; + + var candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); + if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; + + dt = dt.DeclaringType; + if (dt == null) + { + return false; + } + + candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); + if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; + + candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); + if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; + + return false; + } + + private static bool TryResolveSourceMethod(IEnumerable candidateMethods, GeneratedNameKind kind, string matchHint, ref MethodBase method, ref Type type, out int? ordinal) + { + ordinal = null; + foreach (var candidateMethod in candidateMethods) + { + var nethodBody = candidateMethod.GetMethodBody(); + if (kind == GeneratedNameKind.LambdaMethod) + { + foreach (var v in EnumerableIList.Create(nethodBody?.LocalVariables)) + { + if (v.LocalType == type) + { + GetOrdinal(method, ref ordinal); + + } + method = candidateMethod; + type = method.DeclaringType; + return true; + } + } + + + var rawIL = nethodBody?.GetILAsByteArray(); + if (rawIL == null) continue; + + var reader = new ILReader(rawIL); + while (reader.Read(candidateMethod)) + { + if (reader.Operand is MethodBase mb) + { + if (method == mb || (matchHint != null && method.Name.Contains(matchHint))) + { + if (kind == GeneratedNameKind.LambdaMethod) + { + GetOrdinal(method, ref ordinal); + } + + method = candidateMethod; + type = method.DeclaringType; + return true; + } + } + else if (reader.Operand is Type t) + { + if (t == type) + { + method = candidateMethod; + type = method.DeclaringType; + return true; + } + } + } + } + return false; + } + + private static void GetOrdinal(MethodBase method, ref int? ordinal) + { + var lamdaStart = method.Name.IndexOf((char) GeneratedNameKind.LambdaMethod + "__") + 3; + if (lamdaStart > 3) + { + var secondStart = method.Name.IndexOf("_", lamdaStart) + 1; + if (secondStart > 0) + { + lamdaStart = secondStart; + } + + if (!int.TryParse(method.Name.Substring(lamdaStart), out var foundOrdinal)) + { + ordinal = null; + return; + } + + ordinal = foundOrdinal; + + var methods = method.DeclaringType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + var startName = method.Name.Substring(0, lamdaStart); + var count = 0; + foreach (var m in methods) + { + if (m.Name.Length > lamdaStart && m.Name.StartsWith(startName)) + { + count++; + + if (count > 1) + { + break; + } + } + } + + + if (count <= 1) + { + ordinal = null; + } + } + } + + static string GetMatchHint(GeneratedNameKind kind, MethodBase method) + { + var methodName = method.Name; + + switch (kind) + { + case GeneratedNameKind.LocalFunction: + var start = methodName.IndexOf("|"); + if (start < 1) return null; + var end = methodName.IndexOf("_", start) + 1; + if (end <= start) return null; + + return methodName.Substring(start, end - start); + } + return null; + } + + // Parse the generated name. Returns true for names of the form + // [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain + // generated names, where [middle] and [__[suffix]] are optional, + // and where c is a single character in [1-9a-z] + // (csharp\LanguageAnalysis\LIB\SpecialName.cpp). + internal static bool TryParseGeneratedName( + string name, + out GeneratedNameKind kind, + out int openBracketOffset, + out int closeBracketOffset) + { + openBracketOffset = -1; + if (name.StartsWith("CS$<", StringComparison.Ordinal)) + { + openBracketOffset = 3; + } + else if (name.StartsWith("<", StringComparison.Ordinal)) + { + openBracketOffset = 0; + } + + if (openBracketOffset >= 0) + { + closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>'); + if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length) + { + int c = name[closeBracketOffset + 1]; + if ((c >= '1' && c <= '9') || (c >= 'a' && c <= 'z')) // Note '0' is not special. + { + kind = (GeneratedNameKind)c; + return true; + } + } + } + + kind = GeneratedNameKind.None; + openBracketOffset = -1; + closeBracketOffset = -1; + return false; + } + + + private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) + { + char opening = str[openingOffset]; + + int depth = 1; + for (int i = openingOffset + 1; i < str.Length; i++) + { + var c = str[i]; + if (c == opening) + { + depth++; + } + else if (c == closing) + { + depth--; + if (depth == 0) + { + return i; + } + } + } + + return -1; + } + + private static string GetPrefix(ParameterInfo parameter, Type parameterType) + { + if (parameter.IsOut) + { + return "out"; + } + else if (parameterType != null && parameterType.IsByRef) + { + var attribs = parameter.GetCustomAttributes(inherit: false); + if (attribs?.Length > 0) + { + foreach (var attrib in attribs) + { + if (attrib is Attribute att && att.GetType().Namespace == "System.Runtime.CompilerServices" && att.GetType().Name == "IsReadOnlyAttribute") + { + return "in"; + } + } + } + + return "ref"; + } + + return string.Empty; + } + + private static ResolvedParameter GetParameter(ParameterInfo parameter) + { + var parameterType = parameter.ParameterType; + + var prefix = GetPrefix(parameter, parameterType); + + var parameterTypeString = "?"; + if (parameterType != null) + { + if (parameterType.IsGenericType) + { + var tupleNames = parameter.GetCustomAttributes().FirstOrDefault()?.TransformNames; + if (tupleNames != null) + { + var sb = new StringBuilder(); + sb.Append("("); + var args = parameterType.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + sb.Append(TypeNameHelper.GetTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true)); + + if (i >= tupleNames.Count) continue; + + var argName = tupleNames[i]; + if (argName != null) + { + sb.Append(" "); + sb.Append(argName); + } + } + + sb.Append(")"); + parameterTypeString = sb.ToString(); + + return new ResolvedParameter + { + Prefix = prefix, + Name = parameter.Name, + Type = parameterTypeString, + }; + } + } + + if (parameterType.IsByRef) + { + parameterType = parameterType.GetElementType(); + } + + parameterTypeString = TypeNameHelper.GetTypeDisplayName(parameterType, fullName: false, includeGenericParameterNames: true); + + } + + return new ResolvedParameter + { + Prefix = prefix, + Name = parameter.Name, + Type = parameterTypeString, + }; + } + + private static bool ShowInStackTrace(MethodBase method) + { + Debug.Assert(method != null); + try + { + // Don't show any methods marked with the StackTraceHiddenAttribute + // https://github.com/dotnet/coreclr/pull/14652 + foreach (var attibute in EnumerableIList.Create(method.GetCustomAttributesData())) + { + // internal Attribute, match on name + if (attibute.AttributeType.Name == "StackTraceHiddenAttribute") + { + return false; + } + } + + var type = method.DeclaringType; + if (type == null) + { + return true; + } + + foreach (var attibute in EnumerableIList.Create(type.GetCustomAttributesData())) + { + // internal Attribute, match on name + if (attibute.AttributeType.Name == "StackTraceHiddenAttribute") + { + return false; + } + } + + // Fallbacks for runtime pre-StackTraceHiddenAttribute + if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw") + { + return false; + } + else if (type == typeof(TaskAwaiter) || + type == typeof(TaskAwaiter<>) || + type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter)) + { + switch (method.Name) + { + case "HandleNonSuccessAndDebuggerNotification": + case "ThrowForNonSuccess": + case "ValidateEnd": + case "GetResult": + return false; + } + } + else if (type.FullName == "System.ThrowHelper") + { + return false; + } + } + catch + { + // GetCustomAttributesData can throw + return true; + } + + return true; + } + + private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) + { + Debug.Assert(method != null); + Debug.Assert(method.DeclaringType != null); + + declaringType = method.DeclaringType; + + var parentType = declaringType.DeclaringType; + if (parentType == null) + { + return false; + } + + var methods = parentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (methods == null) + { + return false; + } + + foreach (var candidateMethod in methods) + { + var attributes = candidateMethod.GetCustomAttributes(); + if (attributes == null) + { + continue; + } + + foreach (var asma in attributes) + { + if (asma.StateMachineType == declaringType) + { + method = candidateMethod; + declaringType = candidateMethod.DeclaringType; + // Mark the iterator as changed; so it gets the + annotation of the original method + // async statemachines resolve directly to their builder methods so aren't marked as changed + return asma is IteratorStateMachineAttribute; + } + } + } + + return false; + } + + internal enum GeneratedNameKind + { + None = 0, + + // Used by EE: + ThisProxyField = '4', + HoistedLocalField = '5', + DisplayClassLocalOrField = '8', + LambdaMethod = 'b', + LambdaDisplayClass = 'c', + StateMachineType = 'd', + LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names + + // Used by EnC: + AwaiterField = 'u', + HoistedSynthesizedLocalField = 's', + + // Currently not parsed: + StateMachineStateField = '1', + IteratorCurrentBackingField = '2', + StateMachineParameterProxyField = '3', + ReusableHoistedLocalField = '7', + LambdaCacheField = '9', + FixedBufferField = 'e', + AnonymousType = 'f', + TransparentIdentifier = 'h', + AnonymousTypeField = 'i', + AutoPropertyBackingField = 'k', + IteratorCurrentThreadIdField = 'l', + IteratorFinallyMethod = 'm', + BaseMethodWrapper = 'n', + AsyncBuilderField = 't', + DynamicCallSiteContainerType = 'o', + DynamicCallSiteField = 'p' + } + } +} diff --git a/src/Ben.Demystifier/EnhancedStackTrace.cs b/src/Ben.Demystifier/EnhancedStackTrace.cs new file mode 100644 index 0000000..12b3449 --- /dev/null +++ b/src/Ben.Demystifier/EnhancedStackTrace.cs @@ -0,0 +1,113 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Generic.Enumerable; +using System.Diagnostics; +using System.Text; + +namespace System.Diagnostics +{ + public partial class EnhancedStackTrace : StackTrace, IEnumerable + { + private readonly List _frames; + + // Summary: + // Initializes a new instance of the System.Diagnostics.StackTrace class using the + // provided exception object. + // + // Parameters: + // e: + // The exception object from which to construct the stack trace. + // + // Exceptions: + // T:System.ArgumentNullException: + // The parameter e is null. + public EnhancedStackTrace(Exception e) + { + if (e == null) + { + throw new ArgumentNullException(nameof(e)); + } + + _frames = GetFrames(e); + } + + /// + /// Gets the number of frames in the stack trace. + /// + /// The number of frames in the stack trace. + public override int FrameCount => _frames.Count; + + /// + /// Gets the specified stack frame. + /// + /// The index of the stack frame requested. + /// The specified stack frame. + public override StackFrame GetFrame(int index) => _frames[index]; + + /// + /// Returns a copy of all stack frames in the current stack trace. + /// + /// + /// An array of type System.Diagnostics.StackFrame representing the function calls + /// in the stack trace. + /// + public override StackFrame[] GetFrames() => _frames.ToArray(); + + /// + /// Builds a readable representation of the stack trace. + /// + /// A readable representation of the stack trace. + public override string ToString() + { + if (_frames == null) return ""; + + var sb = new StringBuilder(); + + Append(sb); + + return sb.ToString(); + } + + + internal void Append(StringBuilder sb) + { + var frames = _frames; + var count = frames.Count; + + for (var i = 0; i < count; i++) + { + if (i > 0) + { + sb.AppendLine(); + } + + var frame = frames[i]; + + sb.Append(" at "); + frame.MethodInfo.Append(sb); + + var filePath = frame.GetFileName(); + if (!string.IsNullOrEmpty(filePath)) + { + sb.Append(" in "); + sb.Append(System.IO.Path.GetFullPath(filePath)); + } + + var lineNo = frame.GetFileLineNumber(); + if (lineNo != 0) + { + sb.Append(":line "); + sb.Append(lineNo); + } + } + } + + EnumerableIList GetEnumerator() => EnumerableIList.Create(_frames); + IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); + } +} diff --git a/src/Ben.Demystifier/Enumerable/EnumerableIList.cs b/src/Ben.Demystifier/Enumerable/EnumerableIList.cs new file mode 100644 index 0000000..6dc5bf7 --- /dev/null +++ b/src/Ben.Demystifier/Enumerable/EnumerableIList.cs @@ -0,0 +1,68 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Collections.Generic.Enumerable +{ + public static class EnumerableIList + { + public static EnumerableIList Create(IList list) => new EnumerableIList(list); + } + + public readonly struct EnumerableIList : IEnumerableIList, IList + { + private readonly IList _list; + + public EnumerableIList(IList list) + { + _list = list; + } + + public EnumeratorIList GetEnumerator() => new EnumeratorIList(_list); + + public static implicit operator EnumerableIList(List list) => new EnumerableIList(list); + + public static implicit operator EnumerableIList(T[] array) => new EnumerableIList(array); + + public static EnumerableIList Empty = default; + + + // IList pass through + + /// + public T this[int index] { get => _list[index]; set => _list[index] = value; } + + /// + public int Count => _list.Count; + + /// + public bool IsReadOnly => _list.IsReadOnly; + + /// + public void Add(T item) => _list.Add(item); + + /// + public void Clear() => _list.Clear(); + + /// + public bool Contains(T item) => _list.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + /// + public int IndexOf(T item) => _list.IndexOf(item); + + /// + public void Insert(int index, T item) => _list.Insert(index, item); + + /// + public bool Remove(T item) => _list.Remove(item); + + /// + public void RemoveAt(int index) => _list.RemoveAt(index); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Ben.Demystifier/Enumerable/EnumeratorIList.cs b/src/Ben.Demystifier/Enumerable/EnumeratorIList.cs new file mode 100644 index 0000000..c7bb704 --- /dev/null +++ b/src/Ben.Demystifier/Enumerable/EnumeratorIList.cs @@ -0,0 +1,30 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Collections.Generic.Enumerable +{ + public struct EnumeratorIList : IEnumerator + { + private readonly IList _list; + private int _index; + + public EnumeratorIList(IList list) + { + _index = -1; + _list = list; + } + + public T Current => _list[_index]; + + public bool MoveNext() + { + _index++; + + return _index < (_list?.Count ?? 0); + } + + public void Dispose() { } + object IEnumerator.Current => Current; + public void Reset() => _index = -1; + } +} diff --git a/src/Ben.Demystifier/Enumerable/IEnumerableIList.cs b/src/Ben.Demystifier/Enumerable/IEnumerableIList.cs new file mode 100644 index 0000000..93f6947 --- /dev/null +++ b/src/Ben.Demystifier/Enumerable/IEnumerableIList.cs @@ -0,0 +1,10 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Collections.Generic.Enumerable +{ + interface IEnumerableIList : IEnumerable + { + new EnumeratorIList GetEnumerator(); + } +} diff --git a/src/Ben.Demystifier/ExceptionExtentions.cs b/src/Ben.Demystifier/ExceptionExtentions.cs new file mode 100644 index 0000000..8c32c1e --- /dev/null +++ b/src/Ben.Demystifier/ExceptionExtentions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +namespace System.Diagnostics +{ + public static class ExceptionExtentions + { + private static readonly FieldInfo stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); + + public static T Demystify(this T exception) where T : Exception + { + try + { + var stackTrace = new EnhancedStackTrace(exception); + + stackTraceString.SetValue(exception, stackTrace.ToString()); + exception.InnerException?.Demystify(); + } + catch + { + // Processing exceptions shouldn't throw exceptions; if it fails + } + + return exception; + } + } +} diff --git a/src/Ben.Demystifier/Internal/ILReader.cs b/src/Ben.Demystifier/Internal/ILReader.cs new file mode 100644 index 0000000..b347270 --- /dev/null +++ b/src/Ben.Demystifier/Internal/ILReader.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace System.Diagnostics.Internal +{ + internal class ILReader + { + private static OpCode[] singleByteOpCode; + private static OpCode[] doubleByteOpCode; + + private readonly byte[] _cil; + private int ptr; + + + public ILReader(byte[] cil) + { + _cil = cil; + } + + public OpCode OpCode { get; private set; } + public int MetadataToken { get; private set; } + public object Operand { get; private set; } + + public bool Read(MethodBase methodInfo) + { + if (ptr < _cil.Length) + { + OpCode = ReadOpCode(); + Operand = ReadOperand(OpCode, methodInfo); + return true; + } + return false; + } + + OpCode ReadOpCode() + { + byte instruction = ReadByte(); + if (instruction != 254) + return singleByteOpCode[instruction]; + else + return doubleByteOpCode[ReadByte()]; + } + + object ReadOperand(OpCode code, MethodBase methodInfo) + { + MetadataToken = 0; + switch (code.OperandType) + { + case OperandType.InlineMethod: + MetadataToken = ReadInt(); + Type[] methodArgs = null; + if (methodInfo.GetType() != typeof(ConstructorInfo) && !methodInfo.GetType().IsSubclassOf(typeof(ConstructorInfo))) + methodArgs = methodInfo.GetGenericArguments(); + Type[] typeArgs = null; + if (methodInfo.DeclaringType != null) + typeArgs = methodInfo.DeclaringType.GetGenericArguments(); + return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs); + } + return null; + } + + byte ReadByte() + { + return _cil[ptr++]; + } + + int ReadInt() + { + byte b1 = ReadByte(); + byte b2 = ReadByte(); + byte b3 = ReadByte(); + byte b4 = ReadByte(); + return (int)b1 | (((int)b2) << 8) | (((int)b3) << 16) | (((int)b4) << 24); + } + + static ILReader() + { + singleByteOpCode = new OpCode[225]; + doubleByteOpCode = new OpCode[31]; + + FieldInfo[] fields = GetOpCodeFields(); + + for (int i = 0; i < fields.Length; i++) + { + OpCode code = (OpCode)fields[i].GetValue(null); + if (code.OpCodeType == OpCodeType.Nternal) + continue; + + if (code.Size == 1) + singleByteOpCode[code.Value] = code; + else + doubleByteOpCode[code.Value & 0xff] = code; + } + } + + static FieldInfo[] GetOpCodeFields() + { + return typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static); + } + } +} diff --git a/src/Ben.Demystifier/Internal/PortablePdbReader.cs b/src/Ben.Demystifier/Internal/PortablePdbReader.cs new file mode 100644 index 0000000..d9ce03f --- /dev/null +++ b/src/Ben.Demystifier/Internal/PortablePdbReader.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace System.Diagnostics.Internal +{ + // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.StackTrace.Sources/StackFrame/PortablePdbReader.cs + internal class PortablePdbReader : IDisposable + { + private readonly Dictionary _cache = + new Dictionary(StringComparer.Ordinal); + + public void PopulateStackFrame(StackFrame frameInfo, MethodBase method, int IlOffset, out string fileName, out int row, out int column) + { + fileName = ""; + row = 0; + column = 0; + + if (method.Module.Assembly.IsDynamic) + { + return; + } + + var metadataReader = GetMetadataReader(method.Module.Assembly.Location); + + if (metadataReader == null) + { + return; + } + + var methodToken = MetadataTokens.Handle(method.MetadataToken); + + Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition); + + var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle(); + + if (!handle.IsNil) + { + var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle); + var sequencePoints = methodDebugInfo.GetSequencePoints(); + SequencePoint? bestPointSoFar = null; + + foreach (var point in sequencePoints) + { + if (point.Offset > IlOffset) + { + break; + } + + if (point.StartLine != SequencePoint.HiddenLine) + { + bestPointSoFar = point; + } + } + + if (bestPointSoFar.HasValue) + { + row = bestPointSoFar.Value.StartLine; + column = bestPointSoFar.Value.StartColumn; + fileName = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name); + } + } + } + + private MetadataReader GetMetadataReader(string assemblyPath) + { + MetadataReaderProvider provider = null; + if (!_cache.TryGetValue(assemblyPath, out provider)) + { + var pdbPath = GetPdbPath(assemblyPath); + + if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath)) + { + var pdbStream = File.OpenRead(pdbPath); + provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); + } + + _cache[assemblyPath] = provider; + } + + return provider?.GetMetadataReader(); + } + + private static string GetPdbPath(string assemblyPath) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + return null; + } + + if (File.Exists(assemblyPath)) + { + var peStream = File.OpenRead(assemblyPath); + + using (var peReader = new PEReader(peStream)) + { + foreach (var entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + var peDirectory = Path.GetDirectoryName(assemblyPath); + return Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path)); + } + } + } + } + + return null; + } + + private static bool IsPortable(string pdbPath) + { + using (var pdbStream = File.OpenRead(pdbPath)) + { + return pdbStream.ReadByte() == 'B' && + pdbStream.ReadByte() == 'S' && + pdbStream.ReadByte() == 'J' && + pdbStream.ReadByte() == 'B'; + } + } + + public void Dispose() + { + foreach (var entry in _cache) + { + entry.Value?.Dispose(); + } + + _cache.Clear(); + } + } +} diff --git a/src/Ben.Demystifier/Internal/TypeNameHelper.cs b/src/Ben.Demystifier/Internal/TypeNameHelper.cs new file mode 100644 index 0000000..217b183 --- /dev/null +++ b/src/Ben.Demystifier/Internal/TypeNameHelper.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text; + +namespace System.Diagnostics.Internal +{ + // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.TypeNameHelper.Sources/TypeNameHelper.cs + internal class TypeNameHelper + { + private static readonly Dictionary _builtInTypeNames = new Dictionary + { + { typeof(void), "void" }, + { typeof(bool), "bool" }, + { typeof(byte), "byte" }, + { typeof(char), "char" }, + { typeof(decimal), "decimal" }, + { typeof(double), "double" }, + { typeof(float), "float" }, + { typeof(int), "int" }, + { typeof(long), "long" }, + { typeof(object), "object" }, + { typeof(sbyte), "sbyte" }, + { typeof(short), "short" }, + { typeof(string), "string" }, + { typeof(uint), "uint" }, + { typeof(ulong), "ulong" }, + { typeof(ushort), "ushort" } + }; + + /// + /// Pretty print a type name. + /// + /// The . + /// true to print a fully qualified name. + /// true to include generic parameter names. + /// The pretty printed type name. + public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false) + { + var builder = new StringBuilder(); + ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); + return builder.ToString(); + } + + private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options) + { + if (type.IsGenericType) + { + var genericArguments = type.GetGenericArguments(); + ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options); + } + else if (type.IsArray) + { + ProcessArrayType(builder, type, options); + } + else if (_builtInTypeNames.TryGetValue(type, out var builtInName)) + { + builder.Append(builtInName); + } + else if (type.Namespace == nameof(System)) + { + builder.Append(type.Name); + } + else if (type.IsGenericParameter) + { + if (options.IncludeGenericParameterNames) + { + builder.Append(type.Name); + } + } + else + { + builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name); + } + } + + private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options) + { + var innerType = type; + while (innerType.IsArray) + { + innerType = innerType.GetElementType(); + } + + ProcessType(builder, innerType, options); + + while (type.IsArray) + { + builder.Append('['); + builder.Append(',', type.GetArrayRank() - 1); + builder.Append(']'); + type = type.GetElementType(); + } + } + + private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options) + { + var offset = 0; + if (type.IsNested) + { + offset = type.DeclaringType.GetGenericArguments().Length; + } + + if (options.FullName) + { + if (type.IsNested) + { + ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options); + builder.Append('+'); + } + else if (!string.IsNullOrEmpty(type.Namespace)) + { + builder.Append(type.Namespace); + builder.Append('.'); + } + } + + var genericPartIndex = type.Name.IndexOf('`'); + if (genericPartIndex <= 0) + { + builder.Append(type.Name); + return; + } + + builder.Append(type.Name, 0, genericPartIndex); + + builder.Append('<'); + for (var i = offset; i < length; i++) + { + ProcessType(builder, genericArguments[i], options); + if (i + 1 == length) + { + continue; + } + + builder.Append(','); + if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter) + { + builder.Append(' '); + } + } + builder.Append('>'); + } + + private struct DisplayNameOptions + { + public DisplayNameOptions(bool fullName, bool includeGenericParameterNames) + { + FullName = fullName; + IncludeGenericParameterNames = includeGenericParameterNames; + } + + public bool FullName { get; } + + public bool IncludeGenericParameterNames { get; } + } + } +} diff --git a/src/Ben.Demystifier/ResolvedMethod.cs b/src/Ben.Demystifier/ResolvedMethod.cs new file mode 100644 index 0000000..2962259 --- /dev/null +++ b/src/Ben.Demystifier/ResolvedMethod.cs @@ -0,0 +1,147 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic.Enumerable; +using System.Reflection; +using System.Text; + +namespace System.Diagnostics +{ + public class ResolvedMethod + { + public MethodBase MethodBase { get; set; } + + public string DeclaringTypeName { get; set; } + + public bool IsAsync { get; set; } + + public bool IsLambda { get; set; } + + public ResolvedParameter ReturnParameter { get; set; } + + public string Name { get; set; } + + public int? Ordinal { get; set; } + + public string GenericArguments { get; set; } + + public MethodBase SubMethodBase { get; set; } + + public string SubMethod { get; set; } + + public EnumerableIList Parameters { get; set; } + + public EnumerableIList SubMethodParameters { get; set; } + + public override string ToString() => Append(new StringBuilder()).ToString(); + + internal StringBuilder Append(StringBuilder builder) + { + + if (IsAsync) + { + builder + .Append("async "); + } + + if (ReturnParameter != null) + { + ReturnParameter.Append(builder); + builder.Append(" "); + } + + if (!string.IsNullOrEmpty(DeclaringTypeName)) + { + + if (Name == ".ctor") + { + if (string.IsNullOrEmpty(SubMethod) && !IsLambda) + builder.Append("new "); + + builder.Append(DeclaringTypeName); + } + else if (Name == ".cctor") + { + builder.Append("static "); + builder.Append(DeclaringTypeName); + } + else + { + builder + .Append(DeclaringTypeName) + .Append(".") + .Append(Name); + } + } + else + { + builder.Append(Name); + } + builder.Append(GenericArguments); + + builder.Append("("); + if (MethodBase != null) + { + var isFirst = true; + foreach(var param in Parameters) + { + if (isFirst) + { + isFirst = false; + } + else + { + builder.Append(", "); + } + param.Append(builder); + } + } + else + { + builder.Append("?"); + } + builder.Append(")"); + + if (!string.IsNullOrEmpty(SubMethod) || IsLambda) + { + builder.Append("+"); + builder.Append(SubMethod); + builder.Append("("); + if (SubMethodBase != null) + { + var isFirst = true; + foreach (var param in SubMethodParameters) + { + if (isFirst) + { + isFirst = false; + } + else + { + builder.Append(", "); + } + param.Append(builder); + } + } + else + { + builder.Append("?"); + } + builder.Append(")"); + if (IsLambda) + { + builder.Append("=>{}"); + + if (Ordinal.HasValue) + { + builder.Append(" ["); + builder.Append(Ordinal); + builder.Append("]"); + } + } + } + + return builder; + } + } +} diff --git a/src/Ben.Demystifier/ResolvedParameter.cs b/src/Ben.Demystifier/ResolvedParameter.cs new file mode 100644 index 0000000..b662706 --- /dev/null +++ b/src/Ben.Demystifier/ResolvedParameter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Ben A Adams. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; + +namespace System.Diagnostics +{ + public class ResolvedParameter + { + public string Name { get; set; } + + public string Type { get; set; } + + public string Prefix { get; set; } + + public override string ToString() => Append(new StringBuilder()).ToString(); + + internal StringBuilder Append(StringBuilder sb) + { + if (!string.IsNullOrEmpty(Prefix)) + { + sb.Append(Prefix) + .Append(" "); + } + + sb.Append(Type); + if (!string.IsNullOrEmpty(Name)) + { + sb.Append(" ") + .Append(Name); + } + + return sb; + } + } +} diff --git a/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj new file mode 100644 index 0000000..c5e256c --- /dev/null +++ b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + diff --git a/test/Ben.Demystifier.Test/MixedStack.cs b/test/Ben.Demystifier.Test/MixedStack.cs new file mode 100644 index 0000000..a86ec4f --- /dev/null +++ b/test/Ben.Demystifier.Test/MixedStack.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +namespace Demystify +{ + public class MixedStack + { + [Fact] + public void ProducesReadableFrames() + { + // Arrange + Exception exception = GetMixedStackException(); + + // Act + var methodNames = new EnhancedStackTrace(exception) + .Select( + stackFrame => stackFrame.MethodInfo.ToString() + ) + // Remove Framework method that can be optimized out (inlined) + .Where(methodName => methodName != "System.Collections.Generic.List+Enumerator.MoveNext()") + // Don't include this method as call stack shared between multiple tests + .SkipLast(1); + + foreach (var method in methodNames) + { + Console.WriteLine(method.ToString()); + } + // Assert + Assert.Equal (ExpectedCallStack, methodNames.ToList()); + } + + + Exception GetMixedStackException() + { + Exception exception = null; + try + { + Start((val:"", true)); + } + catch (Exception ex) + { + exception = ex; + } + + return exception; + } + + static List ExpectedCallStack = new List() + { + "bool System.Collections.Generic.List+Enumerator.MoveNextRare()", + "IEnumerable Demystify.MixedStack.Iterator()+MoveNext()", + "string string.Join(string separator, IEnumerable values)", + "string Demystify.MixedStack+GenericClass.GenericMethod(ref V value)", + "async Task Demystify.MixedStack.MethodAsync(int value)", + "async Task Demystify.MixedStack.MethodAsync(TValue value)", + "(string val, bool) Demystify.MixedStack.Method(string value)", + "ref string Demystify.MixedStack.RefMethod(string value)", + "(string val, bool) Demystify.MixedStack.s_func(string s, bool b)", + "void Demystify.MixedStack.s_action(string s, bool b)", + "void Demystify.MixedStack.Start((string val, bool) param)" + + }; + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static IEnumerable Iterator() + { + var list = new List() { 1, 2, 3, 4 }; + foreach (var item in list) + { + // Throws the exception + list.Add(item); + + yield return item.ToString(); + } + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static async Task MethodAsync(int value) + { + await Task.Delay(0); + return GenericClass.GenericMethod(ref value); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static async Task MethodAsync(TValue value) + { + return await MethodAsync(1); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static (string val, bool) Method(string value) + { + return (MethodAsync(value).GetAwaiter().GetResult(), true); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static ref string RefMethod(string value) + { + Method(value); + return ref s; + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static void Start((string val, bool) param) + { + s_action.Invoke(param.val, param.Item2); + } + + static Action s_action = (string s, bool b) => s_func(s, b); + static Func s_func = (string s, bool b) => (RefMethod(s), b); + static string s = ""; + + class GenericClass + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static string GenericMethod(ref V value) + { + var returnVal = ""; + for (var i = 0; i < 10; i++) + { + returnVal += string.Join(", ", Iterator()); + } + return returnVal; + } + } + } +}