UNPKG

chrome-devtools-frontend

Version:
135 lines (110 loc) 5.74 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 * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as StackTrace from '../stack_trace/stack_trace.js'; import type * as Workspace from '../workspace/workspace.js'; import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; import {type LiveLocation, LiveLocationPool} from './LiveLocation.js'; export function isErrorLike(stack: string): boolean { return /\n\s*at\s/.test(stack) || stack.startsWith('SyntaxError:'); } export type SymbolizedError = SymbolizedErrorObject|UnparsableError; export class UnparsableError extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { readonly errorStack: string; readonly cause: SymbolizedError|null; constructor(errorStack: string, cause: SymbolizedError|null) { super(); this.errorStack = errorStack; this.cause = cause; this.cause?.addEventListener(Events.UPDATED, this.#fireUpdated, this); } dispose(): void { this.cause?.removeEventListener(Events.UPDATED, this.#fireUpdated, this); if (this.cause instanceof SymbolizedErrorObject || this.cause instanceof UnparsableError) { this.cause.dispose(); } } #fireUpdated(): void { this.dispatchEventToListeners(Events.UPDATED); } } export class SymbolizedErrorObject extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { readonly message: string; readonly stackTrace: StackTrace.StackTrace.ParsedErrorStackTrace; readonly cause: SymbolizedError|null; #syntaxErrorLocation: Workspace.UISourceCode.UILocation|null = null; constructor(message: string, stackTrace: StackTrace.StackTrace.ParsedErrorStackTrace, cause: SymbolizedError|null) { super(); this.message = message; this.stackTrace = stackTrace; this.cause = cause; this.stackTrace.addEventListener(StackTrace.StackTrace.Events.UPDATED, this.#fireUpdated, this); this.cause?.addEventListener(Events.UPDATED, this.#fireUpdated, this); } dispose(): void { this.stackTrace.removeEventListener(StackTrace.StackTrace.Events.UPDATED, this.#fireUpdated, this); this.cause?.removeEventListener(Events.UPDATED, this.#fireUpdated, this); if (this.cause instanceof SymbolizedErrorObject || this.cause instanceof UnparsableError) { this.cause.dispose(); } } get syntaxErrorLocation(): Workspace.UISourceCode.UILocation|null { return this.#syntaxErrorLocation; } /** * Evaluates if we should populate the `syntaxErrorLocation` based on the provided exception details. * * There are three primary cases for SyntaxError: * 1. Programmatic `SyntaxError`: Thrown via `throw new SyntaxError('...', {cause: ...})`. Has a full stack trace, * and an optional cause. The exception details point to the `throw` statement, which is identical to the top frame. * We do NOT want to populate `syntaxErrorLocation` here to avoid redundant location rendering in the UI. * 2. Script parse failure: Failed to parse a script. Has no stack trace but possesses a compile-time location. * We DO want to populate `syntaxErrorLocation` to highlight where the parse failed. * 3. `eval` parse failure: Failed to parse an eval string. Has a stack trace pointing to the `eval` call site * and a compile-time location of the parse failure within the string. The exception details location differs * from the top frame. We DO want to populate `syntaxErrorLocation` here. */ static async createForSyntaxError(target: SDK.Target.Target, debuggerWorkspaceBinding: DebuggerWorkspaceBinding, message: string, exceptionDetails: Protocol.Runtime.ExceptionDetails, stackTrace: StackTrace.StackTrace.ParsedErrorStackTrace, cause: SymbolizedError|null): Promise<SymbolizedErrorObject> { const {exception, scriptId, lineNumber, columnNumber} = exceptionDetails; if (!exception || exception.subtype !== 'error' || exception.className !== 'SyntaxError') { throw new Error('SymbolizedErrorObject.createForSyntaxError expects a SyntaxError'); } const symbolizedError = new SymbolizedErrorObject(message, stackTrace, cause); if (!scriptId) { return symbolizedError; } const topFrame = exceptionDetails.stackTrace?.callFrames[0]; const isProgrammaticThrow = topFrame && topFrame.scriptId === scriptId && topFrame.lineNumber === lineNumber && topFrame.columnNumber === columnNumber; if (!isProgrammaticThrow) { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); if (debuggerModel) { const rawLocation = debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber); // We don't implement dispose here. We won't create many of these so a couple // LiveLocationPools and SymbolizedErrorObject instances leaking is fine. await debuggerWorkspaceBinding.createLiveLocation( rawLocation, symbolizedError.#updateSyntaxErrorLocation.bind(symbolizedError), new LiveLocationPool()); } } return symbolizedError; } async #updateSyntaxErrorLocation(liveLocation: LiveLocation): Promise<void> { this.#syntaxErrorLocation = await liveLocation.uiLocation(); this.dispatchEventToListeners(Events.UPDATED); } #fireUpdated(): void { this.dispatchEventToListeners(Events.UPDATED); } } export const enum Events { UPDATED = 'UPDATED', } export interface EventTypes { [Events.UPDATED]: void; }