UNPKG

chrome-devtools-frontend

Version:
201 lines (178 loc) • 6.68 kB
// Copyright 2026 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import type * as Platform from '../../core/platform/platform.js'; import type * as Protocol from '../../generated/protocol.js'; import type {RawFrame} from './Trie.js'; const CALL_FRAME_REGEX = /^\s*at\s+/; /** * Takes a V8 Error#stack string and extracts structured information. * * @returns Null if the provided string has an unexpected format. A * populated `RawFrame[]` otherwise. */ export function parseRawFramesFromErrorStack(stack: string): RawFrame[]|null { const lines = stack.split('\n'); const firstAtLineIndex = findFramesStartLine(lines); const rawFrames: RawFrame[] = []; if (firstAtLineIndex === -1) { return rawFrames; } for (let i = firstAtLineIndex; i < lines.length; ++i) { const line = lines[i]; const match = CALL_FRAME_REGEX.exec(line); if (!match) { if (line.trim() === '') { continue; } return null; } let lineContent = line.substring(match[0].length); let isAsync = false; if (lineContent.startsWith('async ')) { isAsync = true; lineContent = lineContent.substring(6); } let isConstructor = false; if (lineContent.startsWith('new ')) { isConstructor = true; lineContent = lineContent.substring(4); } let functionName = ''; let url = ''; let lineNumber = -1; let columnNumber = -1; let typeName: string|undefined; let methodName: string|undefined; let isEval = false; let isWasm = false; let wasmModuleName: string|undefined; let wasmFunctionIndex: number|undefined; let promiseIndex: number|undefined; let evalOrigin: RawFrame|undefined; const openParenIndex = lineContent.indexOf(' ('); if (lineContent.endsWith(')') && openParenIndex !== -1) { functionName = lineContent.substring(0, openParenIndex).trim(); let location = lineContent.substring(openParenIndex + 2, lineContent.length - 1); if (location.startsWith('eval at ')) { isEval = true; const commaIndex = location.lastIndexOf(', '); let evalOriginStr = location; if (commaIndex !== -1) { evalOriginStr = location.substring(0, commaIndex); location = location.substring(commaIndex + 2); } else { location = ''; } if (evalOriginStr.startsWith('eval at ')) { evalOriginStr = evalOriginStr.substring(8); } const innerOpenParen = evalOriginStr.indexOf(' ('); let evalFunctionName = evalOriginStr; let evalLocation = ''; if (innerOpenParen !== -1) { evalFunctionName = evalOriginStr.substring(0, innerOpenParen).trim(); evalLocation = evalOriginStr.substring(innerOpenParen + 2, evalOriginStr.length - 1); evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName} (${evalLocation})`)?.[0]; } else { evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName}`)?.[0]; } } if (location.startsWith('index ')) { promiseIndex = parseInt(location.substring(6), 10); url = ''; } else if (location === '<anonymous>' || location === 'native') { url = ''; } else if (location.includes(':wasm-function[')) { isWasm = true; const wasmMatch = /^(.*):wasm-function\[(\d+)\]:(0x[0-9a-fA-F]+)$/.exec(location); if (wasmMatch) { url = wasmMatch[1]; wasmFunctionIndex = parseInt(wasmMatch[2], 10); columnNumber = parseInt(wasmMatch[3], 16); } } else { const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(location); url = splitResult.url; lineNumber = splitResult.lineNumber ?? -1; columnNumber = splitResult.columnNumber ?? -1; } } else { const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(lineContent); url = splitResult.url; lineNumber = splitResult.lineNumber ?? -1; columnNumber = splitResult.columnNumber ?? -1; } // Handle "typeName.methodName [as alias]" if (functionName) { const aliasMatch = /(.*)\s+\[as\s+(.*)\]/.exec(functionName); if (aliasMatch) { methodName = aliasMatch[2]; functionName = aliasMatch[1]; } const dotIndex = functionName.indexOf('.'); if (dotIndex !== -1) { typeName = functionName.substring(0, dotIndex); methodName = methodName ?? functionName.substring(dotIndex + 1); } if (isWasm && typeName) { wasmModuleName = typeName; } } rawFrames.push({ url: url as Platform.DevToolsPath.UrlString, functionName, lineNumber, columnNumber, parsedFrameInfo: { isAsync, isConstructor, isEval, evalOrigin, isWasm, wasmModuleName, wasmFunctionIndex, typeName, methodName, promiseIndex, }, }); } return rawFrames; } function findFramesStartLine(lines: string[]): number { return lines.findIndex(line => CALL_FRAME_REGEX.test(line)); } export function parseMessage(stack: string): string { const lines = stack.split('\n'); const firstAtLineIndex = findFramesStartLine(lines); if (firstAtLineIndex !== -1) { return lines.slice(0, firstAtLineIndex).join('\n'); } return stack; } /** * Error#stack output only contains script URLs. In some cases we are able to * retrieve additional exception details from V8 that we can use to augment * the parsed Error#stack with script IDs. */ export function augmentRawFramesWithScriptIds( rawFrames: RawFrame[], protocolStackTrace: Protocol.Runtime.StackTrace): void { for (const rawFrame of rawFrames) { const isWasm = rawFrame.parsedFrameInfo?.isWasm; const protocolFrame = protocolStackTrace.callFrames.find(frame => { if (isWasm) { // The parser parses Wasm offsets into the `columnNumber` field. The `lineNumber` is always -1. // In the protocol trace, the `lineNumber` is 0 (for Wasm) and `columnNumber` is the bytecode offset. return rawFrame.url === frame.url && rawFrame.columnNumber === frame.columnNumber; } return rawFrame.url === frame.url && rawFrame.lineNumber === frame.lineNumber && rawFrame.columnNumber === frame.columnNumber; }); if (protocolFrame) { // @ts-expect-error scriptId is a readonly property. rawFrame.scriptId = protocolFrame.scriptId; } } }