diff --git a/examples/Console/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs index 4fa2751..9f54766 100644 --- a/examples/Console/Exceptions/Program.cs +++ b/examples/Console/Exceptions/Program.cs @@ -1,11 +1,12 @@ using System; using System.Security.Authentication; +using System.Threading.Tasks; namespace Spectre.Console.Examples; public static class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { try { @@ -43,6 +44,18 @@ public static class Program } }); } + + try + { + await DoMagicAsync(42, null); + } + catch (Exception ex) + { + AnsiConsole.WriteLine(); + AnsiConsole.Write(new Rule("Async").LeftAligned()); + AnsiConsole.WriteLine(); + AnsiConsole.WriteException(ex); + } } private static void DoMagic(int foo, string[,] bar) @@ -61,4 +74,23 @@ public static class Program { throw new InvalidCredentialException("The credentials are invalid."); } + + private static async Task DoMagicAsync(int foo, string[,] bar) + { + try + { + await CheckCredentialsAsync(foo, bar); + } + catch (Exception ex) + { + throw new InvalidOperationException("Whaaat?", ex); + } + } + + private static async Task CheckCredentialsAsync(int qux, string[,] corgi) + { + await Task.Delay(0); + throw new InvalidCredentialException("The credentials are invalid."); + } } + diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs index 4b7c076..3fdcd48 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs @@ -55,14 +55,29 @@ internal static class ExceptionFormatter // Stack frames var stackTrace = new StackTrace(ex, fNeedFileInfo: true); - foreach (var frame in stackTrace.GetFrames().Where(f => f != null).Cast()) + var frames = stackTrace + .GetFrames() + .FilterStackFrames() + .ToList(); + + foreach (var frame in frames) { var builder = new StringBuilder(); // Method var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0; var method = frame.GetMethod(); - var methodName = method.GetName(); + if (method == null) + { + continue; + } + + var methodName = GetMethodName(ref method, out var isAsync); + if (isAsync) + { + builder.Append("async "); + } + builder.Append(Emphasize(methodName, new[] { '.' }, styles.Method, shortenMethods, settings)); builder.AppendWithStyle(styles.Parenthesis, "("); AppendParameters(builder, method, settings); @@ -161,4 +176,158 @@ internal static class ExceptionFormatter return builder.ToString(); } + + private static bool ShowInStackTrace(StackFrame frame) + { + // NET 6 has an attribute of StackTraceHiddenAttribute that we can use to clean up the stack trace + // cleanly. If the user is on an older version we'll fall back to all the stack frames being included. +#if NET6_0_OR_GREATER + var mb = frame.GetMethod(); + if (mb == null) + { + return false; + } + + if ((mb.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0) + { + return false; + } + + try + { + if (mb.IsDefined(typeof(StackTraceHiddenAttribute), false)) + { + return false; + } + + var declaringType = mb.DeclaringType; + if (declaringType?.IsDefined(typeof(StackTraceHiddenAttribute), false) == true) + { + return false; + } + } + catch + { + // if we can't get the attributes then fall back to including it. + } +#endif + + return true; + } + + private static IEnumerable FilterStackFrames(this IEnumerable frames) + { + var allFrames = frames.ToArray(); + var numberOfFrames = allFrames.Length; + + for (var i = 0; i < numberOfFrames; i++) + { + var thisFrame = allFrames[i]; + if (thisFrame == null) + { + continue; + } + + // always include the last frame + if (i == numberOfFrames - 1) + { + yield return thisFrame; + } + else if (ShowInStackTrace(thisFrame)) + { + yield return thisFrame; + } + } + } + + private static string GetMethodName(ref MethodBase method, out bool isAsync) + { + var declaringType = method.DeclaringType; + + if (declaringType?.IsDefined(typeof(CompilerGeneratedAttribute), false) == true) + { + isAsync = typeof(IAsyncStateMachine).IsAssignableFrom(declaringType); + if (isAsync || typeof(IEnumerator).IsAssignableFrom(declaringType)) + { + TryResolveStateMachineMethod(ref method, out declaringType); + } + } + else + { + isAsync = false; + } + + var builder = new StringBuilder(256); + + var fullName = method.DeclaringType?.FullName; + if (fullName != null) + { + // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253 + builder.Append(fullName.Replace('+', '.')); + builder.Append('.'); + } + + builder.Append(method.Name); + if (method.IsGenericMethod) + { + builder.Append('['); + builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name))); + builder.Append(']'); + } + + return builder.ToString(); + } + + private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) + { + // https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L400-L455 + declaringType = method.DeclaringType ?? throw new ArgumentException("Method must have a declaring type.", nameof(method)); + + var parentType = declaringType.DeclaringType; + if (parentType == null) + { + return false; + } + + static IEnumerable GetDeclaredMethods(IReflect type) => type.GetMethods( + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Static | + BindingFlags.Instance | + BindingFlags.DeclaredOnly); + + var methods = GetDeclaredMethods(parentType); + + foreach (var candidateMethod in methods) + { + var attributes = candidateMethod.GetCustomAttributes(false); + + bool foundAttribute = false, foundIteratorAttribute = false; + foreach (var asma in attributes) + { + if (asma.StateMachineType != declaringType) + { + continue; + } + + foundAttribute = true; +#if NET6_0_OR_GREATER + foundIteratorAttribute |= asma is IteratorStateMachineAttribute or AsyncIteratorStateMachineAttribute; +#else + foundIteratorAttribute |= asma is IteratorStateMachineAttribute; +#endif + } + + if (!foundAttribute) + { + continue; + } + + method = candidateMethod; + declaringType = candidateMethod.DeclaringType!; + return foundIteratorAttribute; + } + + return false; + } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Exceptions/MethodExtensions.cs b/src/Spectre.Console/Widgets/Exceptions/MethodExtensions.cs deleted file mode 100644 index ab31cea..0000000 --- a/src/Spectre.Console/Widgets/Exceptions/MethodExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Spectre.Console; - -internal static class MethodExtensions -{ - public static string GetName(this MethodBase? method) - { - if (method is null) - { - return ""; - } - - var builder = new StringBuilder(256); - - var fullName = method.DeclaringType?.FullName; - if (fullName != null) - { - // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253 - builder.Append(fullName.Replace('+', '.')); - builder.Append('.'); - } - - builder.Append(method.Name); - - if (method.IsGenericMethod) - { - builder.Append('['); - builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name))); - builder.Append(']'); - } - - return builder.ToString(); - } -} \ No newline at end of file