Simplify exception formatting

Now that exceptions are not _parsed_ anymore (#513 and #637), keeping the `ExceptionInfo` and `StackFrameInfo` internal classes doesn't make much sense anymore. The `Exception` and `StackFrame` types can be used by the `ExceptionFormatter` class directly, without conversion.
This commit is contained in:
Cédric Luthi 2022-01-19 20:31:45 +01:00 committed by Phil Scott
parent f221c1f25c
commit ff4215f431
6 changed files with 104 additions and 166 deletions

View File

@ -1,57 +0,0 @@
namespace Spectre.Console;
internal static class ExceptionConverter
{
public static ExceptionInfo Convert(Exception exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var exceptionType = exception.GetType();
var stackTrace = new StackTrace(exception, true);
var frames = stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>().Select(Convert).ToList();
var inner = exception.InnerException is null ? null : Convert(exception.InnerException);
return new ExceptionInfo(exceptionType.FullName ?? exceptionType.Name, exception.Message, frames, inner);
}
private static StackFrameInfo Convert(StackFrame frame)
{
var method = frame.GetMethod();
if (method is null)
{
return new StackFrameInfo("<unknown method>", new List<(string Type, string Name)>(), null, null);
}
var methodName = GetMethodName(method);
var parameters = method.GetParameters().Select(e => (e.ParameterType.Name, e.Name ?? string.Empty)).ToList();
var path = frame.GetFileName();
var lineNumber = frame.GetFileLineNumber();
return new StackFrameInfo(methodName, parameters, path, lineNumber == 0 ? null : lineNumber);
}
private static string GetMethodName(MethodBase method)
{
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();
}
}

View File

@ -9,35 +9,35 @@ internal static class ExceptionFormatter
throw new ArgumentNullException(nameof(exception)); throw new ArgumentNullException(nameof(exception));
} }
var info = ExceptionConverter.Convert(exception); return GetException(exception, settings);
return GetException(info, settings);
} }
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings) private static IRenderable GetException(Exception exception, ExceptionSettings settings)
{ {
if (info is null) if (exception is null)
{ {
throw new ArgumentNullException(nameof(info)); throw new ArgumentNullException(nameof(exception));
} }
return new Rows(new IRenderable[] return new Rows(new IRenderable[]
{ {
GetMessage(info, settings), GetMessage(exception, settings),
GetStackFrames(info, settings), GetStackFrames(exception, settings),
}).Expand(); }).Expand();
} }
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings) private static Markup GetMessage(Exception ex, ExceptionSettings settings)
{ {
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0; var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings); var exceptionType = ex.GetType();
var exceptionTypeFullName = exceptionType.FullName ?? exceptionType.Name;
var type = Emphasize(exceptionTypeFullName, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]"; var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message)); return new Markup(string.Concat(type, ": ", message));
} }
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings) private static Grid GetStackFrames(Exception ex, ExceptionSettings settings)
{ {
var styles = settings.Style; var styles = settings.Style;
@ -46,39 +46,44 @@ internal static class ExceptionFormatter
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0)); grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner // Inner
if (ex.Inner != null) if (ex.InnerException != null)
{ {
grid.AddRow( grid.AddRow(
Text.Empty, Text.Empty,
GetException(ex.Inner, settings)); GetException(ex.InnerException, settings));
} }
// Stack frames // Stack frames
foreach (var frame in ex.Frames) var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
foreach (var frame in stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>())
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
// Method // Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0; var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings)); var method = frame.GetMethod();
var methodName = method.GetName();
builder.Append(Emphasize(methodName, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "("); builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings); AppendParameters(builder, method, settings);
builder.AppendWithStyle(styles.Parenthesis, ")"); builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null) var path = frame.GetFileName();
if (path != null)
{ {
builder.Append(' '); builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in"); builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' '); builder.Append(' ');
// Path // Path
AppendPath(builder, frame, settings); AppendPath(builder, path, settings);
// Line number // Line number
if (frame.LineNumber != null) var lineNumber = frame.GetFileLineNumber();
if (lineNumber != 0)
{ {
builder.AppendWithStyle(styles.Dimmed, ":"); builder.AppendWithStyle(styles.Dimmed, ":");
builder.AppendWithStyle(styles.LineNumber, frame.LineNumber); builder.AppendWithStyle(styles.LineNumber, lineNumber);
} }
} }
@ -90,30 +95,28 @@ internal static class ExceptionFormatter
return grid; return grid;
} }
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings) private static void AppendParameters(StringBuilder builder, MethodBase? method, ExceptionSettings settings)
{ {
var typeColor = settings.Style.ParameterType.ToMarkup(); var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup(); var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]"); var parameters = method?.GetParameters().Select(x => $"[{typeColor}]{x.ParameterType.Name.EscapeMarkup()}[/] [{nameColor}]{x.Name?.EscapeMarkup()}[/]");
if (parameters != null)
{
builder.Append(string.Join(", ", parameters)); builder.Append(string.Join(", ", parameters));
} }
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
} }
private static void AppendPath(StringBuilder builder, string path, ExceptionSettings settings)
{
void AppendPath() void AppendPath()
{ {
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0; var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings)); builder.Append(Emphasize(path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
} }
if ((settings.Format & ExceptionFormats.ShowLinks) != 0) if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{ {
var hasLink = frame.TryGetUri(out var uri); var hasLink = path.TryGetUri(out var uri);
if (hasLink && uri != null) if (hasLink && uri != null)
{ {
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']'); builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');

View File

@ -1,20 +0,0 @@
namespace Spectre.Console;
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
}

View File

@ -0,0 +1,33 @@
namespace Spectre.Console;
internal static class MethodExtensions
{
public static string GetName(this MethodBase? method)
{
if (method is null)
{
return "<unknown method>";
}
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();
}
}

View File

@ -1,58 +0,0 @@
namespace Spectre.Console;
internal sealed class StackFrameInfo
{
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}

View File

@ -0,0 +1,37 @@
namespace Spectre.Console;
internal static class StringUriExtensions
{
public static bool TryGetUri(this string path, [NotNullWhen(true)] out Uri? result)
{
try
{
if (!Uri.TryCreate(path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}