318 lines
9.8 KiB
C++
318 lines
9.8 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#include "StackTraceParser.h"
|
|
#include <glog/logging.h>
|
|
#include <charconv>
|
|
#include <optional>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
using namespace facebook::react;
|
|
|
|
const std::string UNKNOWN_FUNCTION = "<unknown>";
|
|
|
|
// TODO(T198763073): Migrate away from std::regex in this file
|
|
// @lint-ignore-every CLANGTIDY facebook-hte-StdRegexIsAwful
|
|
|
|
/**
|
|
* Stack trace parsing for other jsvms:
|
|
* Port of https://github.com/errwischt/stacktrace-parser
|
|
*/
|
|
namespace {
|
|
|
|
std::optional<int> toInt(std::string_view input) {
|
|
int out;
|
|
const std::from_chars_result result =
|
|
std::from_chars(input.data(), input.data() + input.size(), out);
|
|
if (result.ec == std::errc::invalid_argument ||
|
|
result.ec == std::errc::result_out_of_range) {
|
|
return std::nullopt;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
JsErrorHandler::ParsedError::StackFrame parseStackFrame(
|
|
std::string_view file,
|
|
std::string_view methodName,
|
|
std::string_view lineStr,
|
|
std::string_view columnStr) {
|
|
JsErrorHandler::ParsedError::StackFrame frame;
|
|
frame.file = file.empty() ? std::nullopt : std::optional(file);
|
|
frame.methodName = !methodName.empty() ? methodName : UNKNOWN_FUNCTION;
|
|
frame.lineNumber = !lineStr.empty() ? toInt(lineStr) : std::nullopt;
|
|
auto columnOpt = !columnStr.empty() ? toInt(columnStr) : std::nullopt;
|
|
frame.column = columnOpt ? std::optional(*columnOpt - 1) : std::nullopt;
|
|
return frame;
|
|
}
|
|
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> parseChrome(
|
|
const std::string& line) {
|
|
static const std::regex chromeRe(
|
|
R"(^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$)",
|
|
std::regex::icase);
|
|
static const std::regex chromeEvalRe(R"(\((\S*)(?::(\d+))(?::(\d+))\))");
|
|
std::smatch match;
|
|
|
|
if (!std::regex_match(line, match, chromeRe)) {
|
|
return std::nullopt;
|
|
}
|
|
std::string methodName = match[1].str();
|
|
std::string file = match[2].str();
|
|
std::string lineStr = match[3].str();
|
|
std::string columnStr = match[4].str();
|
|
|
|
bool isNative = std::regex_search(file, std::regex("^native"));
|
|
bool isEval = std::regex_search(file, std::regex("^eval"));
|
|
std::string evalFile;
|
|
std::string evalLine;
|
|
std::string evalColumn;
|
|
if (isEval && std::regex_search(file, match, chromeEvalRe)) {
|
|
evalFile = match[1].str();
|
|
evalLine = match[2].str();
|
|
evalColumn = match[3].str();
|
|
file = evalFile;
|
|
lineStr = evalLine;
|
|
columnStr = evalColumn;
|
|
}
|
|
std::string actualFile = !isNative ? file : "";
|
|
return parseStackFrame(actualFile, methodName, lineStr, columnStr);
|
|
}
|
|
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> parseWinjs(
|
|
const std::string& line) {
|
|
static const std::regex winjsRe(
|
|
R"(^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$)",
|
|
std::regex::icase);
|
|
std::smatch match;
|
|
if (!std::regex_match(line, match, winjsRe)) {
|
|
return std::nullopt;
|
|
}
|
|
std::string methodName = match[1].str();
|
|
std::string file = match[2].str();
|
|
std::string lineStr = match[3].str();
|
|
std::string columnStr = match[4].str();
|
|
return parseStackFrame(file, methodName, lineStr, columnStr);
|
|
}
|
|
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> parseGecko(
|
|
const std::string& line) {
|
|
static const std::regex geckoRe(
|
|
R"(^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$)",
|
|
std::regex::icase);
|
|
static const std::regex geckoEvalRe(
|
|
R"((\S+) line (\d+)(?: > eval line \d+)* > eval)", std::regex::icase);
|
|
std::smatch match;
|
|
if (!std::regex_match(line, match, geckoRe)) {
|
|
return std::nullopt;
|
|
}
|
|
std::string methodName = match[1].str();
|
|
std::string tmpStr = match[2].str();
|
|
std::string file = match[3].str();
|
|
std::string lineStr = match[4].str();
|
|
std::string columnStr = match[5].str();
|
|
bool isEval = std::regex_search(file, std::regex(" > eval"));
|
|
std::string evalFile;
|
|
std::string evalLine;
|
|
if (isEval && std::regex_search(file, match, geckoEvalRe)) {
|
|
evalFile = match[1].str();
|
|
evalLine = match[2].str();
|
|
file = evalFile;
|
|
lineStr = evalLine;
|
|
columnStr = ""; // No column number in eval
|
|
}
|
|
return parseStackFrame(file, methodName, lineStr, columnStr);
|
|
}
|
|
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> parseJSC(
|
|
const std::string& line) {
|
|
static const std::regex javaScriptCoreRe(
|
|
R"(^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$)",
|
|
std::regex::icase);
|
|
std::smatch match;
|
|
if (!std::regex_match(line, match, javaScriptCoreRe)) {
|
|
return std::nullopt;
|
|
}
|
|
std::string methodName = match[1].str();
|
|
std::string tmpStr =
|
|
match[2].str(); // This captures any string within parentheses if present
|
|
std::string file = match[3].str();
|
|
std::string lineStr = match[4].str();
|
|
std::string columnStr = match[5].str();
|
|
return parseStackFrame(file, methodName, lineStr, columnStr);
|
|
}
|
|
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> parseNode(
|
|
const std::string& line) {
|
|
static const std::regex nodeRe(
|
|
R"(^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$)",
|
|
std::regex::icase);
|
|
std::smatch match;
|
|
if (!std::regex_match(line, match, nodeRe)) {
|
|
return std::nullopt;
|
|
}
|
|
std::string methodName = match[1].str();
|
|
std::string file = match[2].str();
|
|
std::string lineStr = match[3].str();
|
|
std::string columnStr = match[4].str();
|
|
return parseStackFrame(file, methodName, lineStr, columnStr);
|
|
}
|
|
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> parseOthers(
|
|
const std::string& stackString) {
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> stack;
|
|
std::istringstream iss(stackString);
|
|
std::string line;
|
|
|
|
while (std::getline(iss, line)) {
|
|
std::optional<JsErrorHandler::ParsedError::StackFrame> frame =
|
|
parseChrome(line);
|
|
|
|
if (!frame) {
|
|
frame = parseWinjs(line);
|
|
}
|
|
if (!frame) {
|
|
frame = parseGecko(line);
|
|
}
|
|
if (!frame) {
|
|
frame = parseNode(line);
|
|
}
|
|
if (!frame) {
|
|
frame = parseJSC(line);
|
|
}
|
|
|
|
if (frame) {
|
|
stack.push_back(*frame);
|
|
}
|
|
}
|
|
|
|
return stack;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/**
|
|
* Hermes stack trace parsing logic
|
|
*/
|
|
namespace {
|
|
struct HermesStackLocation {
|
|
std::string type;
|
|
std::string sourceUrl;
|
|
int line1Based{};
|
|
int column1Based{};
|
|
int virtualOffset0Based{};
|
|
};
|
|
|
|
struct HermesStackEntry {
|
|
std::string type;
|
|
std::string functionName;
|
|
HermesStackLocation location;
|
|
int count{};
|
|
};
|
|
|
|
bool isInternalBytecodeSourceUrl(const std::string& sourceUrl) {
|
|
return sourceUrl == "InternalBytecode.js";
|
|
}
|
|
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> convertHermesStack(
|
|
const std::vector<HermesStackEntry>& stack) {
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> frames;
|
|
for (const auto& entry : stack) {
|
|
if (entry.type != "FRAME") {
|
|
continue;
|
|
}
|
|
if (entry.location.type == "NATIVE" ||
|
|
entry.location.type == "INTERNAL_BYTECODE") {
|
|
continue;
|
|
}
|
|
JsErrorHandler::ParsedError::StackFrame frame;
|
|
frame.methodName = entry.functionName;
|
|
frame.file = entry.location.sourceUrl;
|
|
frame.lineNumber = entry.location.line1Based;
|
|
if (entry.location.type == "SOURCE") {
|
|
frame.column = entry.location.column1Based - 1;
|
|
} else {
|
|
frame.column = entry.location.virtualOffset0Based;
|
|
}
|
|
frames.push_back(frame);
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
HermesStackEntry parseLine(const std::string& line) {
|
|
static const std::regex RE_FRAME(
|
|
R"(^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$)");
|
|
static const std::regex RE_SKIPPED(R"(^ {4}... skipping (\d+) frames$)");
|
|
HermesStackEntry entry;
|
|
std::smatch match;
|
|
if (std::regex_match(line, match, RE_FRAME)) {
|
|
entry.type = "FRAME";
|
|
entry.functionName = match[1].str();
|
|
std::string type = match[2].str();
|
|
std::string addressAt = match[3].str();
|
|
std::string sourceUrl = match[4].str();
|
|
if (type == "native") {
|
|
entry.location.type = "NATIVE";
|
|
} else {
|
|
int line1Based = std::stoi(match[5].str());
|
|
int columnOrOffset = std::stoi(match[6].str());
|
|
if (addressAt == "address at ") {
|
|
if (isInternalBytecodeSourceUrl(sourceUrl)) {
|
|
entry.location = {
|
|
"INTERNAL_BYTECODE", sourceUrl, line1Based, 0, columnOrOffset};
|
|
} else {
|
|
entry.location = {
|
|
"BYTECODE", sourceUrl, line1Based, 0, columnOrOffset};
|
|
}
|
|
} else {
|
|
entry.location = {"SOURCE", sourceUrl, line1Based, columnOrOffset, 0};
|
|
}
|
|
}
|
|
return entry;
|
|
}
|
|
if (std::regex_match(line, match, RE_SKIPPED)) {
|
|
entry.type = "SKIPPED";
|
|
entry.count = std::stoi(match[1].str());
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> parseHermes(
|
|
const std::string& stack) {
|
|
static const std::regex RE_COMPONENT_NO_STACK(R"(^ {4}at .*?$)");
|
|
std::istringstream stream(stack);
|
|
std::string line;
|
|
std::vector<HermesStackEntry> entries;
|
|
std::smatch match;
|
|
while (std::getline(stream, line)) {
|
|
if (line.empty()) {
|
|
continue;
|
|
}
|
|
HermesStackEntry entry = parseLine(line);
|
|
if (!entry.type.empty()) {
|
|
entries.push_back(entry);
|
|
continue;
|
|
}
|
|
|
|
if (std::regex_match(line, match, RE_COMPONENT_NO_STACK)) {
|
|
continue;
|
|
}
|
|
entries.clear();
|
|
}
|
|
return convertHermesStack(entries);
|
|
}
|
|
} // namespace
|
|
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> StackTraceParser::parse(
|
|
const bool isHermes,
|
|
const std::string& stackString) {
|
|
std::vector<JsErrorHandler::ParsedError::StackFrame> stackFrames =
|
|
isHermes ? parseHermes(stackString) : parseOthers(stackString);
|
|
return stackFrames;
|
|
}
|