UNPKG

chrome-devtools-frontend

Version:
190 lines (165 loc) 6.69 kB
// Copyright 2024 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 Protocol from '../../generated/protocol.js'; import type * as ScopesCodec from '../../third_party/source-map-scopes-codec/source-map-scopes-codec.js'; import * as i18n from '../i18n/i18n.js'; import type {CallFrame, LocationRange, ScopeChainEntry} from './DebuggerModel.js'; import {type GetPropertiesResult, type RemoteObject, RemoteObjectImpl, RemoteObjectProperty} from './RemoteObject.js'; import {contains} from './SourceMapScopesInfo.js'; const UIStrings = { /** * @description Title of a section in the debugger showing local JavaScript variables. */ local: 'Local', /** * @description Text that refers to closure as a programming term */ closure: 'Closure', /** * @description Noun that represents a section or block of code in the Debugger Model. Shown in the Sources tab, while paused on a breakpoint. */ block: 'Block', /** * @description Title of a section in the debugger showing JavaScript variables from the global scope. */ global: 'Global', /** * @description Text in Scope Chain Sidebar Pane of the Sources panel */ returnValue: 'Return value', } as const; const str_ = i18n.i18n.registerUIStrings('core/sdk/SourceMapScopeChainEntry.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class SourceMapScopeChainEntry implements ScopeChainEntry { readonly #callFrame: CallFrame; readonly #scope: ScopesCodec.OriginalScope; readonly #range?: ScopesCodec.GeneratedRange; readonly #isInnerMostFunction: boolean; readonly #returnValue?: RemoteObject; /** * @param isInnerMostFunction If `scope` is the innermost 'function' scope. Only used for labeling as we name the * scope of the paused function 'Local', while other outer 'function' scopes are named 'Closure'. */ constructor( callFrame: CallFrame, scope: ScopesCodec.OriginalScope, range: ScopesCodec.GeneratedRange|undefined, isInnerMostFunction: boolean, returnValue: RemoteObject|undefined) { this.#callFrame = callFrame; this.#scope = scope; this.#range = range; this.#isInnerMostFunction = isInnerMostFunction; this.#returnValue = returnValue; } extraProperties(): RemoteObjectProperty[] { if (this.#returnValue) { return [new RemoteObjectProperty( i18nString(UIStrings.returnValue), this.#returnValue, undefined, undefined, undefined, undefined, undefined, /* synthetic */ true)]; } return []; } callFrame(): CallFrame { return this.#callFrame; } type(): string { switch (this.#scope.kind) { case 'global': return Protocol.Debugger.ScopeType.Global; case 'function': return this.#isInnerMostFunction ? Protocol.Debugger.ScopeType.Local : Protocol.Debugger.ScopeType.Closure; case 'block': return Protocol.Debugger.ScopeType.Block; } return this.#scope.kind ?? ''; } typeName(): string { switch (this.#scope.kind) { case 'global': return i18nString(UIStrings.global); case 'function': return this.#isInnerMostFunction ? i18nString(UIStrings.local) : i18nString(UIStrings.closure); case 'block': return i18nString(UIStrings.block); } return this.#scope.kind ?? ''; } name(): string|undefined { return this.#scope.name; } range(): LocationRange|null { return null; } object(): RemoteObject { return new SourceMapScopeRemoteObject(this.#callFrame, this.#scope, this.#range); } description(): string { return ''; } icon(): string|undefined { return undefined; } } class SourceMapScopeRemoteObject extends RemoteObjectImpl { readonly #callFrame: CallFrame; readonly #scope: ScopesCodec.OriginalScope; readonly #range?: ScopesCodec.GeneratedRange; constructor(callFrame: CallFrame, scope: ScopesCodec.OriginalScope, range: ScopesCodec.GeneratedRange|undefined) { super( callFrame.debuggerModel.runtimeModel(), /* objectId */ undefined, 'object', /* sub type */ undefined, /* value */ null); this.#callFrame = callFrame; this.#scope = scope; this.#range = range; } override async doGetProperties(_ownProperties: boolean, accessorPropertiesOnly: boolean, generatePreview: boolean): Promise<GetPropertiesResult> { if (accessorPropertiesOnly) { return {properties: [], internalProperties: []}; } const properties: RemoteObjectProperty[] = []; for (const [index, variable] of this.#scope.variables.entries()) { const expression = this.#findExpression(index); if (expression === null) { properties.push(SourceMapScopeRemoteObject.#unavailableProperty(variable)); continue; } // TODO(crbug.com/40277685): Once we can evaluate expressions in scopes other than the innermost one, // we need to find the find the CDP scope that matches `this.#range` and evaluate in that. const result = await this.#callFrame.evaluate({expression, generatePreview}); if ('error' in result || result.exceptionDetails) { // TODO(crbug.com/40277685): Make these errors user-visible to aid tooling developers. // E.g. show the error on hover or expose it in the developer resources panel. properties.push(SourceMapScopeRemoteObject.#unavailableProperty(variable)); } else { properties.push(new RemoteObjectProperty( variable, result.object, /* enumerable */ false, /* writable */ false, /* isOwn */ true, /* wasThrown */ false)); } } return {properties, internalProperties: []}; } /** @returns null if the variable is unavailable at the current paused location */ #findExpression(index: number): string|null { if (!this.#range) { return null; } const expressionOrSubRanges = this.#range.values[index]; if (typeof expressionOrSubRanges === 'string') { return expressionOrSubRanges; } if (expressionOrSubRanges === null) { return null; } const pausedPosition = this.#callFrame.location(); for (const range of expressionOrSubRanges) { if (contains({start: range.from, end: range.to}, pausedPosition.lineNumber, pausedPosition.columnNumber)) { return range.value ?? null; } } return null; } static #unavailableProperty(name: string): RemoteObjectProperty { return new RemoteObjectProperty( name, null, /* enumerable */ false, /* writeable */ false, /* isOwn */ true, /* wasThrown */ false); } }