import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes';
function parseStackTraceFromChromeStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
stack = stack.slice(29);
}
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
for (let i = skipFrames; i < frames.length; i++) {
const parsed = chromeFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
let name = parsed[1] || '';
let isAsync = parsed[8] === 'async ';
if (name === '<anonymous>') {
name = '';
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
let filename = parsed[2] || parsed[5] || '';
if (filename === '<anonymous>') {
filename = '';
}
const line = +(parsed[3] || parsed[6] || 0);
const col = +(parsed[4] || parsed[7] || 0);
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
}
return parsedFrames;
}
const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/;
function parseStackTraceFromFirefoxStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
for (let i = skipFrames; i < frames.length; i++) {
const parsed = firefoxFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
const name = parsed[1] || '';
const filename = parsed[2] || '';
const line = +parsed[3];
const col = +parsed[4];
parsedFrames.push([name, filename, line, col, 0, 0, false]);
}
return parsedFrames;
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
export function parseStackTraceFromString(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.match(CHROME_STACK_REGEXP)) {
return parseStackTraceFromChromeStack(stack, skipFrames);
}
return parseStackTraceFromFirefoxStack(stack, skipFrames);
}
let framesToSkip: number = 0;
let collectedStackTrace: null | ReactStackTrace = null;
const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
function getMethodCallName(callSite: CallSite): string {
const typeName = callSite.getTypeName();
const methodName = callSite.getMethodName();
const functionName = callSite.getFunctionName();
let result = '';
if (functionName) {
if (
typeName &&
identifierRegExp.test(functionName) &&
functionName !== typeName
) {
result += typeName + '.';
}
result += functionName;
if (
methodName &&
functionName !== methodName &&
!functionName.endsWith('.' + methodName) &&
!functionName.endsWith(' ' + methodName)
) {
result += ' [as ' + methodName + ']';
}
} else {
if (typeName) {
result += typeName + '.';
}
if (methodName) {
result += methodName;
} else {
result += '<anonymous>';
}
}
return result;
}
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
const result: ReactStackTrace = [];
for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
let name =
typeof callSite.getFunctionName === 'function'
? callSite.getFunctionName() || '<anonymous>'
: '';
if (
name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame')
) {
break;
} else if (typeof callSite.isNative === 'function' && callSite.isNative()) {
const isAsync =
typeof callSite.isAsync === 'function' && callSite.isAsync();
result.push([name, '', 0, 0, 0, 0, isAsync]);
} else {
if (
typeof callSite.isConstructor === 'function' &&
callSite.isConstructor()
) {
name = 'new ' + name;
} else if (
typeof callSite.isToplevel === 'function' &&
!callSite.isToplevel()
) {
name = getMethodCallName(callSite);
}
if (name === '<anonymous>') {
name = '';
}
let filename =
typeof callSite.getScriptNameOrSourceURL === 'function'
? callSite.getScriptNameOrSourceURL() || '<anonymous>'
: '';
if (filename === '<anonymous>') {
filename = '';
if (typeof callSite.isEval === 'function' && callSite.isEval()) {
const origin =
typeof callSite.getEvalOrigin === 'function'
? callSite.getEvalOrigin()
: null;
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
}
const line =
(typeof callSite.getLineNumber === 'function' &&
callSite.getLineNumber()) ||
0;
const col =
(typeof callSite.getColumnNumber === 'function' &&
callSite.getColumnNumber()) ||
0;
const enclosingLine: number =
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber() || 0
: 0;
const enclosingCol: number =
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber() || 0
: 0;
const isAsync =
typeof callSite.isAsync === 'function' && callSite.isAsync();
result.push([
name,
filename,
line,
col,
enclosingLine,
enclosingCol,
isAsync,
]);
}
}
collectedStackTrace = result;
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
const chromeFrameRegExp =
/^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
export function parseStackTrace(
error: Error,
skipFrames: number,
): ReactStackTrace {
const existing = stackTraceCache.get(error);
if (existing !== undefined) {
return existing;
}
collectedStackTrace = null;
framesToSkip = skipFrames;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = String(error.stack);
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedStackTrace !== null) {
const result = collectedStackTrace;
collectedStackTrace = null;
stackTraceCache.set(error, result);
return result;
}
const parsedFrames = parseStackTraceFromString(stack, skipFrames);
stackTraceCache.set(error, parsedFrames);
return parsedFrames;
}
export function extractLocationFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
const stackTrace = parseStackTrace(error, 1);
const stack = error.stack;
if (
!stack.includes('react_stack_bottom_frame') &&
!stack.includes('react-stack-bottom-frame')
) {
return null;
}
for (let i = stackTrace.length - 1; i >= 0; i--) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
encLine || line,
encCol || col,
];
}
}
return null;
}
export function extractLocationFromComponentStack(
stack: string,
): ReactFunctionLocation | null {
const stackTrace = parseStackTraceFromString(stack, 0);
for (let i = 0; i < stackTrace.length; i++) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
encLine || line,
encCol || col,
];
}
}
return null;
}