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

View File

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