| // 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 protocolFrame = protocolStackTrace.callFrames.find( |
| frame => 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; |
| } |
| } |
| } |