Skip to content

Commit

Permalink
Improves exception rendering for async methods
Browse files Browse the repository at this point in the history
  • Loading branch information
phil-scott-78 authored and patriksvensson committed Feb 3, 2022
1 parent ff4215f commit a0e20f2
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 36 deletions.
34 changes: 33 additions & 1 deletion examples/Console/Exceptions/Program.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -43,6 +44,18 @@ public static void Main(string[] args)
}
});
}

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)
Expand All @@ -61,4 +74,23 @@ private static void CheckCredentials(int qux, string[,] corgi)
{
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.");
}
}

173 changes: 171 additions & 2 deletions src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,29 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings)

// Stack frames
var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
foreach (var frame in stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>())
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);
Expand Down Expand Up @@ -161,4 +176,158 @@ private static string Emphasize(string input, char[] separators, Style color, bo

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<StackFrame> FilterStackFrames(this IEnumerable<StackFrame?> 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<MethodInfo> 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<StateMachineAttribute>(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;
}
}
33 changes: 0 additions & 33 deletions src/Spectre.Console/Widgets/Exceptions/MethodExtensions.cs

This file was deleted.

0 comments on commit a0e20f2

Please sign in to comment.