| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // 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 SDK from '../../core/sdk/sdk.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| |
| // TODO(crbug.com/1253323): Casts to UrlString will be removed from this file when migration to branded types is complete. |
| |
| export interface ParsedErrorFrame { |
| line: string; |
| link?: { |
| url: Platform.DevToolsPath.UrlString, |
| prefix: string, |
| suffix: string, |
| lineNumber?: number, |
| columnNumber?: number, enclosedInBraces: boolean, |
| scriptId?: Protocol.Runtime.ScriptId, |
| }; |
| } |
| |
| /** |
| * Takes a V8 Error#stack string and extracts source position information. |
| * |
| * The result includes the url, line and column number, as well as where |
| * the url is found in the raw line. |
| * |
| * @returns Null if the provided string has an unexpected format. A |
| * populated `ParsedErrorFrame[]` otherwise. |
| */ |
| export function parseSourcePositionsFromErrorStack( |
| runtimeModel: SDK.RuntimeModel.RuntimeModel, stack: string): ParsedErrorFrame[]|null { |
| if (!/^[\w.]*Error\b/.test(stack)) { |
| return null; |
| } |
| const debuggerModel = runtimeModel.debuggerModel(); |
| const baseURL = runtimeModel.target().inspectedURL(); |
| |
| const lines = stack.split('\n'); |
| const linkInfos = []; |
| for (const line of lines) { |
| const isCallFrameLine = /^\s*at\s/.test(line); |
| if (!isCallFrameLine && linkInfos.length && linkInfos[linkInfos.length - 1].link) { |
| return null; |
| } |
| |
| if (!isCallFrameLine) { |
| linkInfos.push({line}); |
| continue; |
| } |
| |
| let openBracketIndex = -1; |
| let closeBracketIndex = -1; |
| const inBracketsWithLineAndColumn = /\([^\)\(]+:\d+:\d+\)/g; |
| const inBrackets = /\([^\)\(]+\)/g; |
| let lastMatch: RegExpExecArray|null = null; |
| let currentMatch; |
| while ((currentMatch = inBracketsWithLineAndColumn.exec(line))) { |
| lastMatch = currentMatch; |
| } |
| if (!lastMatch) { |
| while ((currentMatch = inBrackets.exec(line))) { |
| lastMatch = currentMatch; |
| } |
| } |
| if (lastMatch) { |
| openBracketIndex = lastMatch.index; |
| closeBracketIndex = lastMatch.index + lastMatch[0].length - 1; |
| } |
| const hasOpenBracket = openBracketIndex !== -1; |
| let left = hasOpenBracket ? openBracketIndex + 1 : line.indexOf('at') + 3; |
| if (!hasOpenBracket && line.indexOf('async ') === left) { |
| left += 6; |
| } |
| const right = hasOpenBracket ? closeBracketIndex : line.length; |
| const linkCandidate = line.substring(left, right); |
| const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(linkCandidate); |
| if (splitResult.url === '<anonymous>') { |
| linkInfos.push({line}); |
| continue; |
| } |
| let url = parseOrScriptMatch(debuggerModel, splitResult.url); |
| if (!url && Common.ParsedURL.ParsedURL.isRelativeURL(splitResult.url)) { |
| url = parseOrScriptMatch(debuggerModel, Common.ParsedURL.ParsedURL.completeURL(baseURL, splitResult.url)); |
| } |
| if (!url) { |
| return null; |
| } |
| |
| linkInfos.push({ |
| line, |
| link: { |
| url, |
| prefix: line.substring(0, left), |
| suffix: line.substring(right), |
| enclosedInBraces: hasOpenBracket, |
| lineNumber: splitResult.lineNumber, |
| columnNumber: splitResult.columnNumber, |
| }, |
| }); |
| } |
| return linkInfos; |
| } |
| |
| function parseOrScriptMatch(debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString|null): |
| Platform.DevToolsPath.UrlString|null { |
| if (!url) { |
| return null; |
| } |
| const parsedURL = Common.ParsedURL.ParsedURL.fromString(url); |
| if (parsedURL) { |
| return parsedURL.url as Platform.DevToolsPath.UrlString; |
| } |
| if (debuggerModel.scriptsForSourceURL(url).length) { |
| return url; |
| } |
| // nodejs stack traces contain (absolute) file paths, but v8 reports them as file: urls. |
| const fileUrl = new URL(url, 'file://'); |
| if (debuggerModel.scriptsForSourceURL(fileUrl.href).length) { |
| return fileUrl.href as Platform.DevToolsPath.UrlString; |
| } |
| return null; |
| } |
| |
| /** |
| * 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. |
| * This function sets the `scriptId` field in `ParsedErrorFrame` when it finds |
| * the corresponding info in `Protocol.Runtime.StackTrace`. |
| */ |
| export function augmentErrorStackWithScriptIds( |
| parsedFrames: ParsedErrorFrame[], protocolStackTrace: Protocol.Runtime.StackTrace): void { |
| // Note that the number of frames between the two stack traces can differ. The |
| // parsed Error#stack can contain Builtin frames which are not present in the protocol |
| // stack. This means its easier to always search the whole protocol stack for a matching |
| // frame rather then trying to detect the Builtin frames and skipping them. |
| for (const parsedFrame of parsedFrames) { |
| const protocolFrame = protocolStackTrace.callFrames.find(frame => framesMatch(parsedFrame, frame)); |
| if (protocolFrame && parsedFrame.link) { |
| parsedFrame.link.scriptId = protocolFrame.scriptId; |
| } |
| } |
| } |
| |
| /** Returns true iff both stack frames have the same url and line/column numbers. The function name is ignored */ |
| function framesMatch(parsedFrame: ParsedErrorFrame, protocolFrame: Protocol.Runtime.CallFrame): boolean { |
| if (!parsedFrame.link) { |
| return false; |
| } |
| |
| const {url, lineNumber, columnNumber} = parsedFrame.link; |
| return url === protocolFrame.url && lineNumber === protocolFrame.lineNumber && |
| columnNumber === protocolFrame.columnNumber; |
| } |