/
ashipov
/
react
Обзор
Документация
Войти
/
ashipov
/
react
Код
Запросы
0
Задачи 2.0
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
packages/react-devtools-shared/src/backend/utils/parseStackTrace.js
366 строк
12 KB
Ruslan Lesiutin
fix[devtools]: feature-check structure stack trace methods (#35293)
10 дек 2025, 22:21
Не верифицирован
10 дек 2025, 22:21
5a97093
Код
Авторство
О чём код?
/** /** * 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. * * @flow */ import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes'; function parseStackTraceFromChromeStack( stack: string, skipFrames: number, ): ReactStackTrace { if (stack.startsWith('Error: react-stack-top-frame\n')) { // V8's default formatting prefixes with the error message which we // don't want/need. 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) { // Cut off everything after the bottom frame since it'll be internals. stack = stack.slice(0, idx); } const frames = stack.split('\n'); const parsedFrames: ReactStackTrace = []; // We skip top frames here since they may or may not be parseable but we // want to skip the same number of frames regardless. I.e. we can't do it // in the caller. 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) { // Cut off everything after the bottom frame since it'll be internals. stack = stack.slice(0, idx); } const frames = stack.split('\n'); const parsedFrames: ReactStackTrace = []; // We skip top frames here since they may or may not be parseable but we // want to skip the same number of frames regardless. I.e. we can't do it // in the caller. 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 = []; // Collect structured stack traces from the callsites. // We mirror how V8 serializes stack frames and how we later parse them. for (let i = framesToSkip; i < structuredStackTrace.length; i++) { const callSite = structuredStackTrace[i]; let name = // $FlowFixMe[method-unbinding] typeof callSite.getFunctionName === 'function' ? callSite.getFunctionName() || '<anonymous>' : ''; if ( name.includes('react_stack_bottom_frame') || name.includes('react-stack-bottom-frame') ) { // Skip everything after the bottom frame since it'll be internals. break; // $FlowFixMe[method-unbinding] } else if (typeof callSite.isNative === 'function' && callSite.isNative()) { const isAsync = // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-use] typeof callSite.isAsync === 'function' && callSite.isAsync(); result.push([name, '', 0, 0, 0, 0, isAsync]); } else { // We encode complex function calls as if they're part of the function // name since we cannot simulate the complex ones and they look the same // as function names in UIs on the client as well as stacks. if ( // $FlowFixMe[method-unbinding] typeof callSite.isConstructor === 'function' && callSite.isConstructor() ) { name = 'new ' + name; } else if ( // $FlowFixMe[method-unbinding] typeof callSite.isToplevel === 'function' && !callSite.isToplevel() ) { name = getMethodCallName(callSite); } if (name === '<anonymous>') { name = ''; } let filename = // $FlowFixMe[method-unbinding] typeof callSite.getScriptNameOrSourceURL === 'function' ? callSite.getScriptNameOrSourceURL() || '<anonymous>' : ''; if (filename === '<anonymous>') { filename = ''; // $FlowFixMe[method-unbinding] if (typeof callSite.isEval === 'function' && callSite.isEval()) { const origin = // $FlowFixMe[method-unbinding] typeof callSite.getEvalOrigin === 'function' ? callSite.getEvalOrigin() : null; if (origin) { filename = origin.toString() + ', <anonymous>'; } } } const line = // $FlowFixMe[method-unbinding] (typeof callSite.getLineNumber === 'function' && callSite.getLineNumber()) || 0; const col = // $FlowFixMe[method-unbinding] (typeof callSite.getColumnNumber === 'function' && callSite.getColumnNumber()) || 0; const enclosingLine: number = // $FlowFixMe[prop-missing] typeof callSite.getEnclosingLineNumber === 'function' ? (callSite: any).getEnclosingLineNumber() || 0 : 0; const enclosingCol: number = // $FlowFixMe[prop-missing] typeof callSite.getEnclosingColumnNumber === 'function' ? (callSite: any).getEnclosingColumnNumber() || 0 : 0; const isAsync = // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-use] typeof callSite.isAsync === 'function' && callSite.isAsync(); result.push([ name, filename, line, col, enclosingLine, enclosingCol, isAsync, ]); } } collectedStackTrace = result; // At the same time we generate a string stack trace just in case someone // else reads it. Ideally, we'd call the previous prepareStackTrace to // ensure it's in the expected format but it's common for that to be // source mapped and since we do a lot of eager parsing of errors, it // would be slow in those environments. We could maybe just rely on those // environments having to disable source mapping globally to speed things up. // For now, we just generate a default V8 formatted stack trace without // source mapping as a fallback. 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; } // This matches either of these V8 formats. // at name (filename:0:0) // at filename:0:0 // at async filename:0:0 // at Array.map (<anonymous>) 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 { // We can only get structured data out of error objects once. So we cache the information // so we can get it again each time. It also helps performance when the same error is // referenced more than once. const existing = stackTraceCache.get(error); if (existing !== undefined) { return existing; } // We override Error.prepareStackTrace with our own version that collects // the structured data. We need more information than the raw stack gives us // and we need to ensure that we don't get the source mapped version. 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; } // If the stack has already been read, or this is not actually a V8 compatible // engine then we might not get a normalized stack and it might still have been // source mapped. Regardless we try our best to parse it. 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') ) { // This didn't have a bottom to it, we can't trust it. return null; } // We start from the bottom since that will have the best location for the owner itself. for (let i = stackTrace.length - 1; i >= 0; i--) { const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; // Take the first match with a colon in the file name. if (fileName.indexOf(':') !== -1) { return [ functionName, fileName, // Use enclosing line if available, since that points to the start of the function. 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]; // Take the first match with a colon in the file name. if (fileName.indexOf(':') !== -1) { return [ functionName, fileName, // Use enclosing line if available. (Never the case here because we parse from string.) encLine || line, encCol || col, ]; } } return null; }