mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 21:38:16 +08:00
Add support for rendering exceptions
This commit is contained in:

committed by
Patrik Svensson

parent
971f9032ba
commit
3c3afe7439
159
src/Spectre.Console/Internal/ExceptionFormatter.cs
Normal file
159
src/Spectre.Console/Internal/ExceptionFormatter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
23
src/Spectre.Console/Internal/ExceptionInfo.cs
Normal file
23
src/Spectre.Console/Internal/ExceptionInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
142
src/Spectre.Console/Internal/ExceptionParser.cs
Normal file
142
src/Spectre.Console/Internal/ExceptionParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
65
src/Spectre.Console/Internal/StackFrameInfo.cs
Normal file
65
src/Spectre.Console/Internal/StackFrameInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user