Add support for rendering exceptions

This commit is contained in:
Patrik Svensson
2020-10-03 02:08:31 +02:00
committed by Patrik Svensson
parent 971f9032ba
commit 3c3afe7439
35 changed files with 926 additions and 41 deletions

View File

@ -0,0 +1,159 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal static class ExceptionFormatter
{
private static readonly Color _typeColor = Color.White;
private static readonly Color _methodColor = Color.Yellow;
private static readonly Color _parameterColor = Color.Blue;
private static readonly Color _pathColor = Color.Yellow;
private static readonly Color _dimmedColor = Color.Grey;
public static IRenderable Format(Exception exception, ExceptionFormats format)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionParser.Parse(exception.ToString());
if (info == null)
{
return new Text(exception.ToString());
}
return GetException(info, format);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionFormats format)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, format),
GetStackFrames(info, format),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionFormats format)
{
var shortenTypes = (format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, _typeColor.ToMarkupString(), shortenTypes);
var message = $"[b red]{ex.Message.SafeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionFormats format)
{
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, format));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, _methodColor.ToMarkupString(), shortenMethods));
builder.Append('(');
builder.Append(string.Join(", ", frame.Parameters.Select(x => $"[{_parameterColor.ToMarkupString()}]{x.Type.SafeMarkup()}[/] {x.Name}")));
builder.Append(')');
if (frame.Path != null)
{
builder.Append(" [").Append(_dimmedColor.ToMarkupString()).Append("]in[/] ");
// Path
AppendPath(builder, frame, format);
// Line number
if (frame.LineNumber != null)
{
builder.Append(':');
builder.Append('[').Append(_parameterColor.ToMarkupString()).Append(']').Append(frame.LineNumber).Append("[/]");
}
}
grid.AddRow($"[{_dimmedColor.ToMarkupString()}]at[/]", builder.ToString());
}
return grid;
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionFormats format)
{
if (frame?.Path is null)
{
return;
}
void RenderLink()
{
var shortenPaths = (format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, $"b {_pathColor.ToMarkupString()}", shortenPaths));
}
if ((format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
RenderLink();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
RenderLink();
}
}
private static string Emphasize(string input, char[] separators, string color, bool compact)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.Append("[silver]").Append(type, 0, index + 1).Append("[/]");
}
builder.Append('[').Append(color).Append(']').Append(type, index + 1, type.Length - index - 1).Append("[/]");
}
else
{
builder.Append(type);
}
return builder.ToString();
}
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
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,142 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Spectre.Console.Internal
{
internal static class ExceptionParser
{
private static readonly Regex _messageRegex = new Regex(@"^(?'type'.*):\s(?'message'.*)$");
private static readonly Regex _stackFrameRegex = new Regex(@"^\s*\w*\s(?'method'.*)\((?'params'.*)\)");
private static readonly Regex _fullStackFrameRegex = new Regex(@"^\s*(?'at'\w*)\s(?'method'.*)\((?'params'.*)\)\s(?'in'\w*)\s(?'path'.*)\:(?'line'\w*)\s(?'linenumber'\d*)$");
public static ExceptionInfo? Parse(string exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var lines = exception.SplitLines();
return Parse(new Queue<string>(lines));
}
private static ExceptionInfo? Parse(Queue<string> lines)
{
if (lines.Count == 0)
{
// Error: No lines to parse
return null;
}
var line = lines.Dequeue();
line = line.Replace(" ---> ", string.Empty);
var match = _messageRegex.Match(line);
if (!match.Success)
{
return null;
}
var inner = (ExceptionInfo?)null;
// Stack frames
var frames = new List<StackFrameInfo>();
while (lines.Count > 0)
{
if (lines.Peek().TrimStart().StartsWith("---> ", StringComparison.OrdinalIgnoreCase))
{
inner = Parse(lines);
if (inner == null)
{
// Error: Could not parse inner exception
return null;
}
continue;
}
line = lines.Dequeue();
if (string.IsNullOrWhiteSpace(line))
{
// Empty line
continue;
}
if (line.TrimStart().StartsWith("--- ", StringComparison.OrdinalIgnoreCase))
{
// End of inner exception
break;
}
var stackFrame = ParseStackFrame(line);
if (stackFrame == null)
{
// Error: Could not parse stack frame
return null;
}
frames.Add(stackFrame);
}
return new ExceptionInfo(
match.Groups["type"].Value,
match.Groups["message"].Value,
frames, inner);
}
private static StackFrameInfo? ParseStackFrame(string frame)
{
var match = _fullStackFrameRegex.Match(frame);
if (match?.Success != true)
{
match = _stackFrameRegex.Match(frame);
if (match?.Success != true)
{
return null;
}
}
var parameters = ParseMethodParameters(match.Groups["params"].Value);
if (parameters == null)
{
// Error: Could not parse parameters
return null;
}
var method = match.Groups["method"].Value;
var path = match.Groups["path"].Success ? match.Groups["path"].Value : null;
var lineNumber = (int?)null;
if (!string.IsNullOrWhiteSpace(match.Groups["linenumber"].Value))
{
lineNumber = int.Parse(match.Groups["linenumber"].Value, CultureInfo.InvariantCulture);
}
return new StackFrameInfo(method, parameters, path, lineNumber);
}
private static List<(string Type, string Name)>? ParseMethodParameters(string parameters)
{
var result = new List<(string Type, string Name)>();
foreach (var parameterPart in parameters.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
{
var parameterNameIndex = parameterPart.LastIndexOf(' ');
if (parameterNameIndex == -1)
{
// Error: Could not parse parameter
return null;
}
var type = parameterPart.Substring(0, parameterNameIndex);
var name = parameterPart.Substring(parameterNameIndex + 1, parameterPart.Length - parameterNameIndex - 1);
result.Add((type, name));
}
return result;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Spectre.Console.Internal
{
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 System.ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new System.ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
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;
}
}
}
}