From 8e762e4618727dca4732f3e3ef0492d5227819d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 23 Nov 2021 14:40:32 +0100 Subject: [PATCH] Simplify stack frame parsing This is the natural extension of #513 which already removed a lot of regex parsing. The `ExceptionParser` class is renamed into `ExceptionConverter` because there is no more string parsing performed at all. `Exception` is converted into `ExceptionInfo` and `StackFrame` is converted into `StackFrameInfo`. --- .../Widgets/Exceptions/ExceptionConverter.cs | 65 +++++++++++++++ .../Widgets/Exceptions/ExceptionFormatter.cs | 2 +- .../Widgets/Exceptions/ExceptionParser.cs | 79 ------------------- 3 files changed, 66 insertions(+), 80 deletions(-) create mode 100644 src/Spectre.Console/Widgets/Exceptions/ExceptionConverter.cs delete mode 100644 src/Spectre.Console/Widgets/Exceptions/ExceptionParser.cs diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionConverter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionConverter.cs new file mode 100644 index 0000000..01ef922 --- /dev/null +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionConverter.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +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().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("", 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(); + } + } +} diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs index e377865..cf8874d 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs @@ -14,7 +14,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(exception)); } - var info = ExceptionParser.Parse(exception); + var info = ExceptionConverter.Convert(exception); return GetException(info, settings); } diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionParser.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionParser.cs deleted file mode 100644 index b143f90..0000000 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionParser.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Spectre.Console -{ - internal static class ExceptionParser - { - 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(Exception exception) - { - if (exception is null) - { - throw new ArgumentNullException(nameof(exception)); - } - - var exceptionType = exception.GetType(); - var frames = exception.StackTrace?.SplitLines().Select(ParseStackFrame).Where(e => e != null).Cast().ToList() ?? new List(); - var inner = exception.InnerException is null ? null : Parse(exception.InnerException); - return new ExceptionInfo(exceptionType.FullName ?? exceptionType.Name, exception.Message, 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; - } - } -}