blob: 56370f5ecd97a3c8db6b7418c64e53bbd087156f [file] [log] [blame]
// 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';
export interface ParsedErrorFrame {
line: string;
isCallFrame?: boolean;
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 match = /^\s*at\s(async\s)?/.exec(line);
if (!match) {
if (linkInfos.length && linkInfos[linkInfos.length - 1].isCallFrame) {
return null;
}
linkInfos.push({line});
continue;
}
const isCallFrame = true;
let left = match[0].length;
let right = line.length;
let enclosedInBraces = false;
while (line[right - 1] === ')') {
right--;
enclosedInBraces = true;
do {
left = line.indexOf('(', left);
if (left < 0) {
return null;
}
left++;
if (!line.substring(left).startsWith('eval at ')) {
break;
}
left += 8;
right = line.lastIndexOf(', ', right) - 1;
if (right < 0) {
return null;
}
} while (true);
}
const linkCandidate = line.substring(left, right);
const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(linkCandidate);
if (splitResult.url === '<anonymous>') {
if (linkInfos.length && linkInfos[linkInfos.length - 1].isCallFrame && !linkInfos[linkInfos.length - 1].link) {
// Combine builtin frames.
linkInfos[linkInfos.length - 1].line += `\n${line}`;
} else {
linkInfos.push({line, isCallFrame});
}
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,
isCallFrame,
link: {
url,
prefix: line.substring(0, left),
suffix: line.substring(right),
enclosedInBraces,
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;
}
if (Common.ParsedURL.ParsedURL.isValidUrlString(url)) {
return url;
}
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;
}