Improve stack trace parsing

This commit is contained in:
Tyrrrz
2020-10-23 22:57:48 +03:00
parent f765af6061
commit d0d024c427
2 changed files with 108 additions and 106 deletions

View File

@@ -160,13 +160,11 @@ namespace CliFx
foreach (var stackFrame in StackFrame.ParseMany(exception.StackTrace))
{
console.Error.Write(indentationShared + indentationLocal);
// "at"
console.Error.Write(stackFrame.Prefix + " ");
console.Error.Write("at ");
// "CliFx.Demo.Commands.BookAddCommand."
console.WithForegroundColor(ConsoleColor.DarkGray, () =>
console.Error.Write(stackFrame.ParentType)
console.Error.Write(stackFrame.ParentType + ".")
);
// "ExecuteAsync"
@@ -176,47 +174,62 @@ namespace CliFx
console.Error.Write("(");
foreach (var parameter in stackFrame.Parameters)
for (var i = 0; i < stackFrame.Parameters.Count; i++)
{
var parameter = stackFrame.Parameters[i];
// "IConsole"
console.WithForegroundColor(ConsoleColor.Blue, () =>
console.Error.Write(parameter.Type)
);
if (!string.IsNullOrWhiteSpace(parameter.Name))
{
console.Error.Write(" ");
// "console"
console.WithForegroundColor(ConsoleColor.White, () =>
console.Error.Write(parameter.Name)
);
}
// ", '
if (parameter.Separator != null)
// Separator
if (stackFrame.Parameters.Count > 1 && i < stackFrame.Parameters.Count - 1)
{
console.Error.Write(parameter.Separator);
console.Error.Write(", ");
}
}
console.Error.Write(") ");
// "in"
console.Error.Write(stackFrame.LocationPrefix);
// Location
if (!string.IsNullOrWhiteSpace(stackFrame.FilePath))
{
console.Error.Write("in");
console.Error.Write("\n" + indentationShared + indentationLocal + indentationLocal);
// "E:\Projects\Softdev\CliFx\CliFx.Demo\Commands\"
var stackFrameDirectoryPath = Path.GetDirectoryName(stackFrame.FilePath);
console.WithForegroundColor(ConsoleColor.DarkGray, () =>
console.Error.Write(stackFrame.DirectoryPath)
console.Error.Write(stackFrameDirectoryPath + Path.DirectorySeparatorChar)
);
// "BookAddCommand.cs"
var stackFrameFileName = Path.GetFileName(stackFrame.FilePath);
console.WithForegroundColor(ConsoleColor.Yellow, () =>
console.Error.Write(stackFrame.FileName)
console.Error.Write(stackFrameFileName)
);
if (!string.IsNullOrWhiteSpace(stackFrame.LineNumber))
{
console.Error.Write(":");
// "35"
console.WithForegroundColor(ConsoleColor.Blue, () =>
console.Error.Write(stackFrame.LineNumber)
);
}
}
console.Error.WriteLine();
}

View File

@@ -10,121 +10,110 @@ namespace CliFx.Internal
{
public string Type { get; }
public string Name { get; }
public string? Name { get; }
public string? Separator { get; }
public StackFrameParameter(
string type,
string name,
string? separator)
public StackFrameParameter(string type, string? name)
{
Type = type;
Name = name;
Separator = separator;
}
}
internal partial class StackFrame
{
public string Prefix { get; }
public string ParentType { get; }
public string MethodName { get; }
public IReadOnlyList<StackFrameParameter> Parameters { get; }
public string LocationPrefix { get; }
public string? FilePath { get; }
public string DirectoryPath { get; }
public string FileName { get; }
public string LineNumber { get; }
public string? LineNumber { get; }
public StackFrame(
string prefix,
string parentType,
string methodName,
IReadOnlyList<StackFrameParameter> parameters,
string locationPrefix,
string directoryPath,
string fileName,
string lineNumber)
string? filePath,
string? lineNumber)
{
Prefix = prefix;
ParentType = parentType;
MethodName = methodName;
Parameters = parameters;
LocationPrefix = locationPrefix;
DirectoryPath = directoryPath;
FileName = fileName;
FilePath = filePath;
LineNumber = lineNumber;
}
}
internal partial class StackFrame
{
private static readonly Regex MethodMatcher =
new Regex(@"(?<prefix>\S+) (?<name>.*?)(?<methodName>[^\.]+)\(");
private const string Space = @"[\x20\t]";
private const string NotSpace = @"[^\x20\t]";
private static readonly Regex ParameterMatcher =
new Regex(@"(?<type>.+? )(?<name>.+?)(?:(?<separator>, )|\))");
private static readonly Regex FileMatcher =
new Regex(@"(?<prefix>\S+?) (?<path>.*?)(?<file>[^\\/]+?(?:\.\w*)?):[^:]+? (?<line>\d+).*");
public static StackFrame Parse(string stackFrame)
{
var methodMatch = MethodMatcher.Match(stackFrame);
var parameterMatches = ParameterMatcher.Matches(stackFrame, methodMatch.Index + methodMatch.Length)
.Cast<Match>()
.ToArray();
var fileMatch = FileMatcher.Match(
stackFrame,
parameterMatches.Length switch
{
0 => methodMatch.Index + methodMatch.Length + 1,
_ => parameterMatches[parameterMatches.Length - 1].Index +
parameterMatches[parameterMatches.Length - 1].Length
}
// Taken from https://github.com/atifaziz/StackTraceParser
private static readonly Regex Pattern = new Regex(@"
^
" + Space + @"*
\w+ " + Space + @"+
(?<frame>
(?<type> " + NotSpace + @"+ ) \.
(?<method> " + NotSpace + @"+? ) " + Space + @"*
(?<params> \( ( " + Space + @"* \)
| (?<pt> .+?) " + Space + @"+ (?<pn> .+?)
(, " + Space + @"* (?<pt> .+?) " + Space + @"+ (?<pn> .+?) )* \) ) )
( " + Space + @"+
( # Microsoft .NET stack traces
\w+ " + Space + @"+
(?<file> ( [a-z] \: # Windows rooted path starting with a drive letter
| / ) # *nix rooted path starting with a forward-slash
.+? )
\: \w+ " + Space + @"+
(?<line> [0-9]+ ) \p{P}?
| # Mono stack traces
\[0x[0-9a-f]+\] " + Space + @"+ \w+ " + Space + @"+
<(?<file> [^>]+ )>
:(?<line> [0-9]+ )
)
)?
)
\s*
$",
RegexOptions.IgnoreCase |
RegexOptions.Multiline |
RegexOptions.ExplicitCapture |
RegexOptions.CultureInvariant |
RegexOptions.IgnorePatternWhitespace,
TimeSpan.FromSeconds(5)
);
// Ensure everything was parsed successfully
var isSuccessful =
methodMatch.Success &&
parameterMatches.All(m => m.Success) &&
fileMatch.Success &&
fileMatch.Index + fileMatch.Length == stackFrame.Length;
if (!isSuccessful)
public static IEnumerable<StackFrame> ParseMany(string stackTrace)
{
throw new FormatException("Failed to parse stack frame.");
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
// Ensure success
var lastMatch = matches.LastOrDefault();
if (lastMatch == null ||
lastMatch.Index + lastMatch.Length < stackTrace.Length)
{
throw new FormatException("Could not parse stack trace.");
}
var parameters = parameterMatches
.Select(match => new StackFrameParameter(
match.Groups["type"].Value,
match.Groups["name"].Value,
match.Groups["separator"].Value.NullIfWhiteSpace()
)).ToArray();
return new StackFrame(
methodMatch.Groups["prefix"].Value,
methodMatch.Groups["name"].Value,
methodMatch.Groups["methodName"].Value,
parameters,
fileMatch.Groups["prefix"].Value,
fileMatch.Groups["path"].Value,
fileMatch.Groups["file"].Value,
fileMatch.Groups["line"].Value
return from m in matches
select m.Groups
into groups
let pt = groups["pt"].Captures
let pn = groups["pn"].Captures
select new StackFrame(
groups["type"].Value,
groups["method"].Value,
(
from i in Enumerable.Range(0, pt.Count)
select new StackFrameParameter(pt[i].Value, pn[i].Value.NullIfWhiteSpace())
).ToArray(),
groups["file"].Value.NullIfWhiteSpace(),
groups["line"].Value.NullIfWhiteSpace()
);
}
public static IReadOnlyList<StackFrame> ParseMany(string stackTrace) =>
stackTrace.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(Parse).ToArray();
}
}