chrome-devtools-frontend
Version:
Chrome DevTools UI
1,192 lines (1,024 loc) • 42 kB
text/typescript
// Copyright 2021 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.
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the #name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import * as Protocol from '../../generated/protocol.js';
import {type DOMPinnedWebIDLProp, type DOMPinnedWebIDLType} from '../common/JavaScriptMetaData.js';
import {type DebuggerModel, type FunctionDetails} from './DebuggerModel.js';
import {type RuntimeModel} from './RuntimeModel.js';
export class RemoteObject {
/**
* This may not be an interface due to "instanceof RemoteObject" checks in the code.
*/
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static fromLocalObject(value: any): RemoteObject {
return new LocalJSONObject(value);
}
static type(remoteObject: RemoteObject): string {
if (remoteObject === null) {
return 'null';
}
const type = typeof remoteObject;
if (type !== 'object' && type !== 'function') {
return type;
}
return remoteObject.type;
}
static isNullOrUndefined(remoteObject?: RemoteObject): boolean {
if (remoteObject === undefined) {
return true;
}
switch (remoteObject.type) {
case Protocol.Runtime.RemoteObjectType.Object:
return remoteObject.subtype === Protocol.Runtime.RemoteObjectSubtype.Null;
case Protocol.Runtime.RemoteObjectType.Undefined:
return true;
default:
return false;
}
}
static arrayNameFromDescription(description: string): string {
return description.replace(_descriptionLengthParenRegex, '').replace(_descriptionLengthSquareRegex, '');
}
static arrayLength(object: RemoteObject|Protocol.Runtime.RemoteObject|Protocol.Runtime.ObjectPreview): number {
if (object.subtype !== 'array' && object.subtype !== 'typedarray') {
return 0;
}
// Array lengths in V8-generated descriptions switched from square brackets to parentheses.
// Both formats are checked in case the front end is dealing with an old version of V8.
const parenMatches = object.description && object.description.match(_descriptionLengthParenRegex);
const squareMatches = object.description && object.description.match(_descriptionLengthSquareRegex);
return parenMatches ? parseInt(parenMatches[1], 10) : (squareMatches ? parseInt(squareMatches[1], 10) : 0);
}
static arrayBufferByteLength(object: RemoteObject|Protocol.Runtime.RemoteObject|
Protocol.Runtime.ObjectPreview): number {
if (object.subtype !== 'arraybuffer') {
return 0;
}
const matches = object.description && object.description.match(_descriptionLengthParenRegex);
return matches ? parseInt(matches[1], 10) : 0;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static unserializableDescription(object: any): string|null {
const type = typeof object;
if (type === 'number') {
const description = String(object);
if (object === 0 && 1 / object < 0) {
return UnserializableNumber.Negative0;
}
if (description === UnserializableNumber.NaN || description === UnserializableNumber.Infinity ||
description === UnserializableNumber.NegativeInfinity) {
return description;
}
}
if (type === 'bigint') {
return object + 'n';
}
return null;
}
static toCallArgument(object: string|number|bigint|boolean|RemoteObject|Protocol.Runtime.RemoteObject|null|
undefined): Protocol.Runtime.CallArgument {
const type = typeof object;
if (type === 'undefined') {
return {};
}
const unserializableDescription = RemoteObject.unserializableDescription(object);
if (type === 'number') {
if (unserializableDescription !== null) {
return {unserializableValue: unserializableDescription};
}
return {value: object};
}
if (type === 'bigint') {
return {unserializableValue: (unserializableDescription as string)};
}
if (type === 'string' || type === 'boolean') {
return {value: object};
}
if (!object) {
return {value: null};
}
// The unserializableValue is a function on RemoteObject's and a simple property on
// Protocol.Runtime.RemoteObject's.
const objectAsProtocolRemoteObject = (object as Protocol.Runtime.RemoteObject);
if (object instanceof RemoteObject) {
const unserializableValue = object.unserializableValue();
if (unserializableValue !== undefined) {
return {unserializableValue: unserializableValue};
}
} else if (objectAsProtocolRemoteObject.unserializableValue !== undefined) {
return {unserializableValue: objectAsProtocolRemoteObject.unserializableValue};
}
if (typeof objectAsProtocolRemoteObject.objectId !== 'undefined') {
return {objectId: objectAsProtocolRemoteObject.objectId};
}
return {value: objectAsProtocolRemoteObject.value};
}
static async loadFromObjectPerProto(
object: RemoteObject, generatePreview: boolean,
nonIndexedPropertiesOnly: boolean = false): Promise<GetPropertiesResult> {
const result = await Promise.all([
object.getAllProperties(true /* accessorPropertiesOnly */, generatePreview, nonIndexedPropertiesOnly),
object.getOwnProperties(generatePreview, nonIndexedPropertiesOnly),
]);
const accessorProperties = result[0].properties;
const ownProperties = result[1].properties;
const internalProperties = result[1].internalProperties;
if (!ownProperties || !accessorProperties) {
return {properties: null, internalProperties: null} as GetPropertiesResult;
}
const propertiesMap = new Map<string, RemoteObjectProperty>();
const propertySymbols = [];
for (let i = 0; i < accessorProperties.length; i++) {
const property = accessorProperties[i];
if (property.symbol) {
propertySymbols.push(property);
} else if (property.isOwn || property.name !== '__proto__') {
// TODO(crbug/1076820): Eventually we should move away from
// showing accessor #properties directly on the receiver.
propertiesMap.set(property.name, property);
}
}
for (let i = 0; i < ownProperties.length; i++) {
const property = ownProperties[i];
if (property.isAccessorProperty()) {
continue;
}
if (property.private || property.symbol) {
propertySymbols.push(property);
} else {
propertiesMap.set(property.name, property);
}
}
return {
properties: [...propertiesMap.values()].concat(propertySymbols),
internalProperties: internalProperties ? internalProperties : null,
};
}
customPreview(): Protocol.Runtime.CustomPreview|null {
return null;
}
get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
// TODO(crbug.com/1226471): Return undefined here.
return 'Not implemented' as Protocol.Runtime.RemoteObjectId;
}
get type(): string {
throw 'Not implemented';
}
get subtype(): string|undefined {
throw 'Not implemented';
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get value(): any {
throw 'Not implemented';
}
unserializableValue(): string|undefined {
throw 'Not implemented';
}
get description(): string|undefined {
throw 'Not implemented';
}
set description(description: string|undefined) {
throw 'Not implemented';
}
get hasChildren(): boolean {
throw 'Not implemented';
}
get preview(): Protocol.Runtime.ObjectPreview|undefined {
return undefined;
}
get className(): string|null {
return null;
}
arrayLength(): number {
throw 'Not implemented';
}
arrayBufferByteLength(): number {
throw 'Not implemented';
}
getOwnProperties(_generatePreview: boolean, _nonIndexedPropertiesOnly?: boolean): Promise<GetPropertiesResult> {
throw 'Not implemented';
}
getAllProperties(_accessorPropertiesOnly: boolean, _generatePreview: boolean, _nonIndexedPropertiesOnly?: boolean):
Promise<GetPropertiesResult> {
throw 'Not implemented';
}
async deleteProperty(_name: Protocol.Runtime.CallArgument): Promise<string|undefined> {
throw 'Not implemented';
}
async setPropertyValue(_name: string|Protocol.Runtime.CallArgument, _value: string): Promise<string|undefined> {
throw 'Not implemented';
}
callFunction<T>(
_functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
_args?: Protocol.Runtime.CallArgument[]): Promise<CallFunctionResult> {
throw 'Not implemented';
}
callFunctionJSON<T>(
_functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
_args: Protocol.Runtime.CallArgument[]|undefined): Promise<T> {
throw 'Not implemented';
}
release(): void {
}
debuggerModel(): DebuggerModel {
throw new Error('DebuggerModel-less object');
}
runtimeModel(): RuntimeModel {
throw new Error('RuntimeModel-less object');
}
isNode(): boolean {
return false;
}
webIdl?: RemoteObjectWebIdlTypeMetadata;
}
export class RemoteObjectImpl extends RemoteObject {
runtimeModelInternal: RuntimeModel;
readonly #runtimeAgent: ProtocolProxyApi.RuntimeApi;
readonly #typeInternal: string;
readonly #subtypeInternal: string|undefined;
#objectIdInternal: Protocol.Runtime.RemoteObjectId|undefined;
#descriptionInternal: string|undefined;
hasChildrenInternal: boolean;
readonly #previewInternal: Protocol.Runtime.ObjectPreview|undefined;
readonly #unserializableValueInternal: string|undefined;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly #valueInternal: any;
readonly #customPreviewInternal: Protocol.Runtime.CustomPreview|null;
readonly #classNameInternal: string|null;
constructor(
runtimeModel: RuntimeModel, objectId: Protocol.Runtime.RemoteObjectId|undefined, type: string,
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subtype: string|undefined, value: any, unserializableValue?: string, description?: string,
preview?: Protocol.Runtime.ObjectPreview, customPreview?: Protocol.Runtime.CustomPreview, className?: string) {
super();
this.runtimeModelInternal = runtimeModel;
this.#runtimeAgent = runtimeModel.target().runtimeAgent();
this.#typeInternal = type;
this.#subtypeInternal = subtype;
if (objectId) {
// handle
this.#objectIdInternal = objectId;
this.#descriptionInternal = description;
this.hasChildrenInternal = (type !== 'symbol');
this.#previewInternal = preview;
} else {
this.#descriptionInternal = description;
if (!this.description && unserializableValue) {
this.#descriptionInternal = unserializableValue;
}
if (!this.#descriptionInternal && (typeof value !== 'object' || value === null)) {
this.#descriptionInternal = String(value);
}
this.hasChildrenInternal = false;
if (typeof unserializableValue === 'string') {
this.#unserializableValueInternal = unserializableValue;
if (unserializableValue === UnserializableNumber.Infinity ||
unserializableValue === UnserializableNumber.NegativeInfinity ||
unserializableValue === UnserializableNumber.Negative0 ||
unserializableValue === UnserializableNumber.NaN) {
this.#valueInternal = Number(unserializableValue);
} else if (type === 'bigint' && unserializableValue.endsWith('n')) {
this.#valueInternal = BigInt(unserializableValue.substring(0, unserializableValue.length - 1));
} else {
this.#valueInternal = unserializableValue;
}
} else {
this.#valueInternal = value;
}
}
this.#customPreviewInternal = customPreview || null;
this.#classNameInternal = typeof className === 'string' ? className : null;
}
override customPreview(): Protocol.Runtime.CustomPreview|null {
return this.#customPreviewInternal;
}
override get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return this.#objectIdInternal;
}
override get type(): string {
return this.#typeInternal;
}
override get subtype(): string|undefined {
return this.#subtypeInternal;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override get value(): any {
return this.#valueInternal;
}
override unserializableValue(): string|undefined {
return this.#unserializableValueInternal;
}
override get description(): string|undefined {
return this.#descriptionInternal;
}
override set description(description: string|undefined) {
this.#descriptionInternal = description;
}
override get hasChildren(): boolean {
return this.hasChildrenInternal;
}
override get preview(): Protocol.Runtime.ObjectPreview|undefined {
return this.#previewInternal;
}
override get className(): string|null {
return this.#classNameInternal;
}
override getOwnProperties(generatePreview: boolean, nonIndexedPropertiesOnly: boolean = false): Promise<GetPropertiesResult> {
return this.doGetProperties(true, false, nonIndexedPropertiesOnly, generatePreview);
}
override getAllProperties(
accessorPropertiesOnly: boolean, generatePreview: boolean,
nonIndexedPropertiesOnly: boolean = false): Promise<GetPropertiesResult> {
return this.doGetProperties(false, accessorPropertiesOnly, nonIndexedPropertiesOnly, generatePreview);
}
async createRemoteObject(object: Protocol.Runtime.RemoteObject): Promise<RemoteObject> {
return this.runtimeModelInternal.createRemoteObject(object);
}
async doGetProperties(
ownProperties: boolean, accessorPropertiesOnly: boolean, nonIndexedPropertiesOnly: boolean,
generatePreview: boolean): Promise<GetPropertiesResult> {
if (!this.#objectIdInternal) {
return {properties: null, internalProperties: null} as GetPropertiesResult;
}
const response = await this.#runtimeAgent.invoke_getProperties({
objectId: this.#objectIdInternal,
ownProperties,
accessorPropertiesOnly,
nonIndexedPropertiesOnly,
generatePreview,
});
if (response.getError()) {
return {properties: null, internalProperties: null} as GetPropertiesResult;
}
if (response.exceptionDetails) {
this.runtimeModelInternal.exceptionThrown(Date.now(), response.exceptionDetails);
return {properties: null, internalProperties: null} as GetPropertiesResult;
}
const {result: properties = [], internalProperties = [], privateProperties = []} = response;
const result = [];
for (const property of properties) {
const propertyValue = property.value ? await this.createRemoteObject(property.value) : null;
const propertySymbol = property.symbol ? this.runtimeModelInternal.createRemoteObject(property.symbol) : null;
const remoteProperty = new RemoteObjectProperty(
property.name, propertyValue, Boolean(property.enumerable), Boolean(property.writable),
Boolean(property.isOwn), Boolean(property.wasThrown), propertySymbol);
if (typeof property.value === 'undefined') {
if (property.get && property.get.type !== 'undefined') {
remoteProperty.getter = this.runtimeModelInternal.createRemoteObject(property.get);
}
if (property.set && property.set.type !== 'undefined') {
remoteProperty.setter = this.runtimeModelInternal.createRemoteObject(property.set);
}
}
result.push(remoteProperty);
}
for (const property of privateProperties) {
const propertyValue = property.value ? this.runtimeModelInternal.createRemoteObject(property.value) : null;
const remoteProperty = new RemoteObjectProperty(
property.name, propertyValue, true, true, true, false, undefined, false, undefined, true);
if (typeof property.value === 'undefined') {
if (property.get && property.get.type !== 'undefined') {
remoteProperty.getter = this.runtimeModelInternal.createRemoteObject(property.get);
}
if (property.set && property.set.type !== 'undefined') {
remoteProperty.setter = this.runtimeModelInternal.createRemoteObject(property.set);
}
}
result.push(remoteProperty);
}
const internalPropertiesResult = [];
for (const property of internalProperties) {
if (!property.value) {
continue;
}
if (property.name === '[[StableObjectId]]') {
continue;
}
const propertyValue = this.runtimeModelInternal.createRemoteObject(property.value);
internalPropertiesResult.push(
new RemoteObjectProperty(property.name, propertyValue, true, false, undefined, undefined, undefined, true));
}
return {properties: result, internalProperties: internalPropertiesResult};
}
override async setPropertyValue(name: string|Protocol.Runtime.CallArgument, value: string): Promise<string|undefined> {
if (!this.#objectIdInternal) {
return 'Can’t set a property of non-object.';
}
const response = await this.#runtimeAgent.invoke_evaluate({expression: value, silent: true});
if (response.getError() || response.exceptionDetails) {
return response.getError() ||
(response.result.type !== 'string' ? response.result.description : response.result.value as string);
}
if (typeof name === 'string') {
name = RemoteObject.toCallArgument(name);
}
const resultPromise = this.doSetObjectPropertyValue(response.result, name);
if (response.result.objectId) {
void this.#runtimeAgent.invoke_releaseObject({objectId: response.result.objectId});
}
return resultPromise;
}
async doSetObjectPropertyValue(result: Protocol.Runtime.RemoteObject, name: Protocol.Runtime.CallArgument):
Promise<string|undefined> {
// This assignment may be for a regular (data) property, and for an accessor property (with getter/setter).
// Note the sensitive matter about accessor property: the property may be physically defined in some proto object,
// but logically it is bound to the object in question. JavaScript passes this object to getters/setters, not the object
// where property was defined; so do we.
const setPropertyValueFunction = 'function(a, b) { this[a] = b; }';
const argv = [name, RemoteObject.toCallArgument(result)];
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectIdInternal,
functionDeclaration: setPropertyValueFunction,
arguments: argv,
silent: true,
});
const error = response.getError();
return error || response.exceptionDetails ? error || response.result.description : undefined;
}
override async deleteProperty(name: Protocol.Runtime.CallArgument): Promise<string|undefined> {
if (!this.#objectIdInternal) {
return 'Can’t delete a property of non-object.';
}
const deletePropertyFunction = 'function(a) { delete this[a]; return !(a in this); }';
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectIdInternal,
functionDeclaration: deletePropertyFunction,
arguments: [name],
silent: true,
});
if (response.getError() || response.exceptionDetails) {
return response.getError() || response.result.description;
}
if (!response.result.value) {
return 'Failed to delete property.';
}
return undefined;
}
override async callFunction<T>(
functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
args?: Protocol.Runtime.CallArgument[]): Promise<CallFunctionResult> {
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectIdInternal,
functionDeclaration: functionDeclaration.toString(),
arguments: args,
silent: true,
});
if (response.getError()) {
return {object: null, wasThrown: false};
}
// TODO: release exceptionDetails object
return {
object: this.runtimeModelInternal.createRemoteObject(response.result),
wasThrown: Boolean(response.exceptionDetails),
};
}
override async callFunctionJSON<T>(
functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
args: Protocol.Runtime.CallArgument[]|undefined): Promise<T> {
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectIdInternal,
functionDeclaration: functionDeclaration.toString(),
arguments: args,
silent: true,
returnByValue: true,
});
return response.getError() || response.exceptionDetails ? null : response.result.value;
}
override release(): void {
if (!this.#objectIdInternal) {
return;
}
void this.#runtimeAgent.invoke_releaseObject({objectId: this.#objectIdInternal});
}
override arrayLength(): number {
return RemoteObject.arrayLength(this);
}
override arrayBufferByteLength(): number {
return RemoteObject.arrayBufferByteLength(this);
}
override debuggerModel(): DebuggerModel {
return this.runtimeModelInternal.debuggerModel();
}
override runtimeModel(): RuntimeModel {
return this.runtimeModelInternal;
}
override isNode(): boolean {
return Boolean(this.#objectIdInternal) && this.type === 'object' && this.subtype === 'node';
}
}
export class ScopeRemoteObject extends RemoteObjectImpl {
#scopeRef: ScopeRef;
#savedScopeProperties: RemoteObjectProperty[]|undefined;
constructor(
runtimeModel: RuntimeModel, objectId: Protocol.Runtime.RemoteObjectId|undefined, scopeRef: ScopeRef, type: string,
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subtype: string|undefined, value: any, unserializableValue?: string, description?: string,
preview?: Protocol.Runtime.ObjectPreview) {
super(runtimeModel, objectId, type, subtype, value, unserializableValue, description, preview);
this.#scopeRef = scopeRef;
this.#savedScopeProperties = undefined;
}
override async doGetProperties(ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean):
Promise<GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: []} as GetPropertiesResult;
}
if (this.#savedScopeProperties) {
// No need to reload scope variables, as the remote object never
// changes its #properties. If variable is updated, the #properties
// array is patched locally.
return {properties: this.#savedScopeProperties.slice(), internalProperties: null};
}
const allProperties = await super.doGetProperties(
ownProperties, accessorPropertiesOnly, false /* nonIndexedPropertiesOnly */, true /* generatePreview */);
if (this.#scopeRef && Array.isArray(allProperties.properties)) {
this.#savedScopeProperties = allProperties.properties.slice();
if (!this.#scopeRef.callFrameId) {
for (const property of this.#savedScopeProperties) {
property.writable = false;
}
}
}
return allProperties;
}
override async doSetObjectPropertyValue(result: Protocol.Runtime.RemoteObject, argumentName: Protocol.Runtime.CallArgument):
Promise<string|undefined> {
const name = (argumentName.value as string);
const error = await this.debuggerModel().setVariableValue(
this.#scopeRef.number, name, RemoteObject.toCallArgument(result),
(this.#scopeRef.callFrameId as Protocol.Debugger.CallFrameId));
if (error) {
return error;
}
if (this.#savedScopeProperties) {
for (const property of this.#savedScopeProperties) {
if (property.name === name) {
property.value = this.runtimeModel().createRemoteObject(result);
}
}
}
return;
}
}
export class ScopeRef {
number: number;
callFrameId: Protocol.Debugger.CallFrameId|undefined;
constructor(number: number, callFrameId?: Protocol.Debugger.CallFrameId) {
this.number = number;
this.callFrameId = callFrameId;
}
}
export class RemoteObjectProperty {
name: string;
value?: RemoteObject;
enumerable: boolean;
writable: boolean;
isOwn: boolean;
wasThrown: boolean;
symbol: RemoteObject|undefined;
synthetic: boolean;
syntheticSetter: ((arg0: string) => Promise<RemoteObject|null>)|undefined;
private: boolean;
getter: RemoteObject|undefined;
setter: RemoteObject|undefined;
webIdl?: RemoteObjectWebIdlPropertyMetadata;
constructor(
name: string, value: RemoteObject|null, enumerable?: boolean, writable?: boolean, isOwn?: boolean,
wasThrown?: boolean, symbol?: RemoteObject|null, synthetic?: boolean,
syntheticSetter?: ((arg0: string) => Promise<RemoteObject|null>), isPrivate?: boolean) {
this.name = name;
this.value = value !== null ? value : undefined;
this.enumerable = typeof enumerable !== 'undefined' ? enumerable : true;
const isNonSyntheticOrSyntheticWritable = !synthetic || Boolean(syntheticSetter);
this.writable = typeof writable !== 'undefined' ? writable : isNonSyntheticOrSyntheticWritable;
this.isOwn = Boolean(isOwn);
this.wasThrown = Boolean(wasThrown);
if (symbol) {
this.symbol = symbol;
}
this.synthetic = Boolean(synthetic);
if (syntheticSetter) {
this.syntheticSetter = syntheticSetter;
}
this.private = Boolean(isPrivate);
}
async setSyntheticValue(expression: string): Promise<boolean> {
if (!this.syntheticSetter) {
return false;
}
const result = await this.syntheticSetter(expression);
if (result) {
this.value = result;
}
return Boolean(result);
}
isAccessorProperty(): boolean {
return Boolean(this.getter || this.setter);
}
match({includeNullOrUndefinedValues, regex}: {includeNullOrUndefinedValues: boolean, regex: RegExp|null}): boolean {
if (regex !== null) {
if (!regex.test(this.name) && !regex.test(this.value?.description ?? '')) {
return false;
}
}
if (!includeNullOrUndefinedValues) {
if (!this.isAccessorProperty() && RemoteObject.isNullOrUndefined(this.value)) {
return false;
}
}
return true;
}
}
// Below is a wrapper around a local object that implements the RemoteObject interface,
// which can be used by the UI code (primarily ObjectPropertiesSection).
// Note that only JSON-compliant objects are currently supported, as there's no provision
// for traversing prototypes, extracting class names via constructor, handling #properties
// or functions.
export class LocalJSONObject extends RemoteObject {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
valueInternal: any;
#cachedDescription!: string;
#cachedChildren!: RemoteObjectProperty[];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(value: any) {
super();
this.valueInternal = value;
}
override get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return undefined;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override get value(): any {
return this.valueInternal;
}
override unserializableValue(): string|undefined {
const unserializableDescription = RemoteObject.unserializableDescription(this.valueInternal);
return unserializableDescription || undefined;
}
override get description(): string {
if (this.#cachedDescription) {
return this.#cachedDescription;
}
function formatArrayItem(this: LocalJSONObject, property: RemoteObjectProperty): string {
return this.formatValue(property.value || null);
}
function formatObjectItem(this: LocalJSONObject, property: RemoteObjectProperty): string {
let name: string = property.name;
if (/^\s|\s$|^$|\n/.test(name)) {
name = '"' + name.replace(/\n/g, '\u21B5') + '"';
}
return name + ': ' + this.formatValue(property.value || null);
}
if (this.type === 'object') {
switch (this.subtype) {
case 'array':
this.#cachedDescription = this.concatenate('[', ']', formatArrayItem.bind(this));
break;
case 'date':
this.#cachedDescription = String(this.valueInternal);
break;
case 'null':
this.#cachedDescription = 'null';
break;
default:
this.#cachedDescription = this.concatenate('{', '}', formatObjectItem.bind(this));
}
} else {
this.#cachedDescription = String(this.valueInternal);
}
return this.#cachedDescription;
}
private formatValue(value: RemoteObject|null): string {
if (!value) {
return 'undefined';
}
const description = value.description || '';
if (value.type === 'string') {
return '"' + description.replace(/\n/g, '\u21B5') + '"';
}
return description;
}
private concatenate(prefix: string, suffix: string, formatProperty: (arg0: RemoteObjectProperty) => string): string {
const previewChars = 100;
let buffer = prefix;
const children = this.children();
for (let i = 0; i < children.length; ++i) {
const itemDescription = formatProperty(children[i]);
if (buffer.length + itemDescription.length > previewChars) {
buffer += ',…';
break;
}
if (i) {
buffer += ', ';
}
buffer += itemDescription;
}
buffer += suffix;
return buffer;
}
override get type(): string {
return typeof this.valueInternal;
}
override get subtype(): string|undefined {
if (this.valueInternal === null) {
return 'null';
}
if (Array.isArray(this.valueInternal)) {
return 'array';
}
if (this.valueInternal instanceof Date) {
return 'date';
}
return undefined;
}
override get hasChildren(): boolean {
if ((typeof this.valueInternal !== 'object') || (this.valueInternal === null)) {
return false;
}
return Boolean(Object.keys((this.valueInternal as Object)).length);
}
override async getOwnProperties(_generatePreview: boolean, nonIndexedPropertiesOnly: boolean = false):
Promise<GetPropertiesResult> {
function isArrayIndex(name: string): boolean {
const index = Number(name) >>> 0;
return String(index) === name;
}
let properties = this.children();
if (nonIndexedPropertiesOnly) {
properties = properties.filter(property => !isArrayIndex(property.name));
}
return {properties, internalProperties: null};
}
override async getAllProperties(
accessorPropertiesOnly: boolean, generatePreview: boolean,
nonIndexedPropertiesOnly: boolean = false): Promise<GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: null};
}
return await this.getOwnProperties(generatePreview, nonIndexedPropertiesOnly);
}
private children(): RemoteObjectProperty[] {
if (!this.hasChildren) {
return [];
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = (this.valueInternal as any);
function buildProperty(propName: string): RemoteObjectProperty {
let propValue = value[propName];
if (!(propValue instanceof RemoteObject)) {
propValue = RemoteObject.fromLocalObject(propValue);
}
return new RemoteObjectProperty(propName, propValue);
}
if (!this.#cachedChildren) {
this.#cachedChildren = Object.keys((value as Object)).map(buildProperty);
}
return this.#cachedChildren;
}
override arrayLength(): number {
return Array.isArray(this.valueInternal) ? this.valueInternal.length : 0;
}
override async callFunction<T>(
functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
args?: Protocol.Runtime.CallArgument[]): Promise<CallFunctionResult> {
const target = (this.valueInternal as Object);
const rawArgs = args ? args.map(arg => arg.value) : [];
let result;
let wasThrown = false;
try {
result = functionDeclaration.apply(target, rawArgs);
} catch (e) {
wasThrown = true;
}
const object = RemoteObject.fromLocalObject(result);
return {object, wasThrown} as CallFunctionResult;
}
override async callFunctionJSON<T>(
functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
args: Protocol.Runtime.CallArgument[]|undefined): Promise<T> {
const target = (this.valueInternal as Object);
const rawArgs = args ? args.map(arg => arg.value) : [];
let result;
try {
result = functionDeclaration.apply(target, rawArgs);
} catch (e) {
result = null;
}
return result as T;
}
}
export class RemoteArrayBuffer {
readonly #objectInternal: RemoteObject;
constructor(object: RemoteObject) {
if (object.type !== 'object' || object.subtype !== 'arraybuffer') {
throw new Error('Object is not an arraybuffer');
}
this.#objectInternal = object;
}
byteLength(): number {
return this.#objectInternal.arrayBufferByteLength();
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async bytes(start: any = 0, end: any = this.byteLength()): Promise<number[]> {
if (start < 0 || start >= this.byteLength()) {
throw new RangeError('start is out of range');
}
if (end < start || end > this.byteLength()) {
throw new RangeError('end is out of range');
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
return await this.#objectInternal.callFunctionJSON(bytes, [{value: start}, {value: end - start}]);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function bytes(this: any, offset: number, length: number): number[] {
return [...new Uint8Array(this, offset, length)];
}
}
object(): RemoteObject {
return this.#objectInternal;
}
}
export class RemoteArray {
readonly #objectInternal: RemoteObject;
constructor(object: RemoteObject) {
this.#objectInternal = object;
}
static objectAsArray(object: RemoteObject|null): RemoteArray {
if (!object || object.type !== 'object' || (object.subtype !== 'array' && object.subtype !== 'typedarray')) {
throw new Error('Object is empty or not an array');
}
return new RemoteArray(object);
}
static createFromRemoteObjects(objects: RemoteObject[]): Promise<RemoteArray> {
if (!objects.length) {
throw new Error('Input array is empty');
}
const objectArguments = [];
for (let i = 0; i < objects.length; ++i) {
objectArguments.push(RemoteObject.toCallArgument(objects[i]));
}
return objects[0].callFunction(createArray, objectArguments).then(returnRemoteArray);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createArray(): any[] {
if (arguments.length > 1) {
return new Array(arguments);
}
return [arguments[0]];
}
function returnRemoteArray(result: CallFunctionResult): RemoteArray {
if (result.wasThrown || !result.object) {
throw new Error('Call function throws exceptions or returns empty value');
}
return RemoteArray.objectAsArray(result.object);
}
}
at(index: number): Promise<RemoteObject> {
if (index < 0 || index > this.#objectInternal.arrayLength()) {
throw new Error('Out of range');
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
return this.#objectInternal.callFunction(at, [RemoteObject.toCallArgument(index)]).then(assertCallFunctionResult);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function at(this: any, index: number): any {
return this[index];
}
function assertCallFunctionResult(result: CallFunctionResult): RemoteObject {
if (result.wasThrown || !result.object) {
throw new Error('Exception in callFunction or result value is empty');
}
return result.object;
}
}
length(): number {
return this.#objectInternal.arrayLength();
}
map<T>(func: (arg0: RemoteObject) => Promise<T>): Promise<T[]> {
const promises = [];
for (let i = 0; i < this.length(); ++i) {
promises.push(this.at(i).then(func));
}
return Promise.all(promises);
}
object(): RemoteObject {
return this.#objectInternal;
}
}
export class RemoteFunction {
readonly #objectInternal: RemoteObject;
constructor(object: RemoteObject) {
this.#objectInternal = object;
}
static objectAsFunction(object: RemoteObject|null): RemoteFunction {
if (!object || object.type !== 'function') {
throw new Error('Object is empty or not a function');
}
return new RemoteFunction(object);
}
targetFunction(): Promise<RemoteObject> {
return this.#objectInternal.getOwnProperties(false /* generatePreview */).then(targetFunction.bind(this));
function targetFunction(this: RemoteFunction, ownProperties: GetPropertiesResult): RemoteObject {
if (!ownProperties.internalProperties) {
return this.#objectInternal;
}
const internalProperties = ownProperties.internalProperties;
for (const property of internalProperties) {
if (property.name === '[[TargetFunction]]') {
return property.value as RemoteObject;
}
}
return this.#objectInternal;
}
}
targetFunctionDetails(): Promise<FunctionDetails|null> {
return this.targetFunction().then(functionDetails.bind(this));
function functionDetails(this: RemoteFunction, targetFunction: RemoteObject): Promise<FunctionDetails|null> {
const boundReleaseFunctionDetails =
releaseTargetFunction.bind(null, this.#objectInternal !== targetFunction ? targetFunction : null);
return targetFunction.debuggerModel().functionDetailsPromise(targetFunction).then(boundReleaseFunctionDetails);
}
function releaseTargetFunction(
targetFunction: RemoteObject|null, functionDetails: FunctionDetails|null): FunctionDetails|null {
if (targetFunction) {
targetFunction.release();
}
return functionDetails;
}
}
object(): RemoteObject {
return this.#objectInternal;
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
const _descriptionLengthParenRegex: RegExp = /\(([0-9]+)\)/;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
const _descriptionLengthSquareRegex: RegExp = /\[([0-9]+)\]/;
const enum UnserializableNumber {
Negative0 = ('-0'),
NaN = ('NaN'),
Infinity = ('Infinity'),
NegativeInfinity = ('-Infinity'),
}
export interface CallFunctionResult {
object: RemoteObject|null;
wasThrown?: boolean;
}
export interface GetPropertiesResult {
properties: RemoteObjectProperty[]|null;
internalProperties: RemoteObjectProperty[]|null;
}
export interface RemoteObjectWebIdlTypeMetadata {
info: DOMPinnedWebIDLType;
state: Map<string, string>;
}
export interface RemoteObjectWebIdlPropertyMetadata {
info: DOMPinnedWebIDLProp;
applicable?: boolean;
}