[go: nahoru, domu]

[LMI] enable object highlighting for new extension API

We recently introduced a new API for extensions to access wasm state and
evaluate code and variables. This change makes object highlighting in
the LMI work with that API, which requires extensions to also report
object sizes in addition to addresses, which was previously missing.

Bug: 1336568, 1299832
Change-Id: I63ad2db35237a0944e09e0b9e62ccc2569d39dca
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3904643
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
Reviewed-by: Kim-Anh Tran <kimanh@chromium.org>
diff --git a/extension-api/ExtensionAPI.d.ts b/extension-api/ExtensionAPI.d.ts
index ff60874..31e16b0 100644
--- a/extension-api/ExtensionAPI.d.ts
+++ b/extension-api/ExtensionAPI.d.ts
@@ -187,6 +187,7 @@
       description?: string;
       objectId?: RemoteObjectId;
       linearMemoryAddress?: number;
+      linearMemorySize?: number;
       hasChildren: boolean;
     }
 
diff --git a/front_end/models/bindings/DebuggerLanguagePlugins.ts b/front_end/models/bindings/DebuggerLanguagePlugins.ts
index 9298aaf..eb41a36a 100644
--- a/front_end/models/bindings/DebuggerLanguagePlugins.ts
+++ b/front_end/models/bindings/DebuggerLanguagePlugins.ts
@@ -796,6 +796,10 @@
     return this.extensionObject.linearMemoryAddress;
   }
 
+  get linearMemorySize(): number|undefined {
+    return this.extensionObject.linearMemorySize;
+  }
+
   get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
     return this.extensionObject.objectId as Protocol.Runtime.RemoteObjectId;
   }
diff --git a/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController.ts b/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController.ts
index 5d8f3a3..1cd7c82 100644
--- a/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController.ts
+++ b/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController.ts
@@ -217,7 +217,10 @@
       Promise<{obj: SDK.RemoteObject.RemoteObject, address: number}|undefined> {
     if (obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject) {
       const valueNode = obj;
-      const address = valueNode.linearMemoryAddress || 0;
+      const address = obj.linearMemoryAddress;
+      if (address === undefined) {
+        return undefined;
+      }
       const callFrame = valueNode.callFrame;
       const response = await obj.debuggerModel().agent.invoke_evaluateOnCallFrame({
         callFrameId: callFrame.id,
@@ -232,7 +235,7 @@
       return {obj: runtimeModel.createRemoteObject(response.result), address};
     }
     if (!(obj instanceof Bindings.DebuggerLanguagePlugins.ValueNode)) {
-      return;
+      return undefined;
     }
 
     const valueNode = obj;
@@ -252,18 +255,20 @@
     return {obj, address};
   }
 
-  // This function returns the size of the source language value represented
-  // by the ValueNode. If the value is a pointer, the function returns the size of
-  // the pointed-to value. If the pointed-to value is also a pointer, it returns
-  // the size of the pointer (usually 4 bytes). This is the convention taken
-  // by the DWARF extension.
+  // This function returns the size of the source language value represented by the ValueNode or ExtensionRemoteObject.
+  // If the value is a pointer, the function returns the size of the pointed-to value. If the pointed-to value is also a
+  // pointer, it returns the size of the pointer (usually 4 bytes). This is the convention taken by the DWARF extension.
   // > double x = 42.0;
   // > double *ptr = &x;
   // > double **dblptr = &ptr;
   //
   // retrieveObjectSize(ptr_ValueNode) -> 8, the size of a double
   // retrieveObjectSize(dblptr_ValueNode) -> 4, the size of a pointer
-  static extractObjectSize(obj: Bindings.DebuggerLanguagePlugins.ValueNode): number {
+  static extractObjectSize(obj: Bindings.DebuggerLanguagePlugins.ValueNode|
+                           Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject): number {
+    if (obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject) {
+      return obj.linearMemorySize ?? 0;
+    }
     let typeInfo = obj.sourceType.typeInfo;
     const pointerMembers = typeInfo.members.filter(member => member.name === '*');
     if (pointerMembers.length === 1) {
@@ -287,7 +292,7 @@
   // -> The memory inspector will jump to the address where 3 is stored.
   // -> The memory inspector will highlight the bytes that represent the 3.
   // -> The object type description of what we show will thus be `int` and not `int *`.
-  static extractObjectTypeDescription(obj: Bindings.DebuggerLanguagePlugins.ValueNode): string {
+  static extractObjectTypeDescription(obj: SDK.RemoteObject.RemoteObject): string {
     const objType = obj.description;
     if (!objType) {
       return '';
@@ -314,7 +319,7 @@
   // Examples:
   // (int *) myNumber -> (int) *myNumber
   // (int[]) numbers -> (int[]) numbers
-  static extractObjectName(obj: Bindings.DebuggerLanguagePlugins.ValueNode, expression: string): string {
+  static extractObjectName(obj: SDK.RemoteObject.RemoteObject, expression: string): string {
     const lastChar = obj.description?.charAt(obj.description.length - 1);
     const isPointerType = lastChar === '*';
     if (isPointerType) {
@@ -375,14 +380,20 @@
   }
 
   static extractHighlightInfo(obj: SDK.RemoteObject.RemoteObject, expression?: string): HighlightInfo|undefined {
-    if (!(obj instanceof Bindings.DebuggerLanguagePlugins.ValueNode)) {
+    if (!(obj instanceof Bindings.DebuggerLanguagePlugins.ValueNode) &&
+        !(obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject)) {
       return undefined;
     }
 
+    const startAddress =
+        (obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject ? obj.linearMemoryAddress :
+                                                                                 obj.inspectableAddress) ??
+        0;
+
     let highlightInfo;
     try {
       highlightInfo = {
-        startAddress: obj.inspectableAddress || 0,
+        startAddress,
         size: LinearMemoryInspectorController.extractObjectSize(obj),
         name: expression ? LinearMemoryInspectorController.extractObjectName(obj, expression) : expression,
         type: LinearMemoryInspectorController.extractObjectTypeDescription(obj),
diff --git a/test/unittests/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController_test.ts b/test/unittests/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController_test.ts
index fdb5b7f..cafd4af 100644
--- a/test/unittests/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController_test.ts
+++ b/test/unittests/front_end/ui/components/linear_memory_inspector/LinearMemoryInspectorController_test.ts
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import * as SDK from '../../../../../../front_end/core/sdk/sdk.js';
-import type * as Bindings from '../../../../../../front_end/models/bindings/bindings.js';
+import * as Bindings from '../../../../../../front_end/models/bindings/bindings.js';
 import * as LinearMemoryInspector from '../../../../../../front_end/ui/components/linear_memory_inspector/linear_memory_inspector.js';
 import {describeWithEnvironment} from '../../../helpers/EnvironmentHelpers.js';
 
@@ -32,6 +32,20 @@
   return new LinearMemoryInspectorController.RemoteArrayBufferWrapper(mockRemoteArrayBuffer);
 }
 
+function createFakeExtensionRemoteObjectFromValueNode(valueNode: Bindings.DebuggerLanguagePlugins.ValueNode) {
+  const {sourceType, inspectableAddress, description} = valueNode;
+  const {typeInfo} = sourceType ?? {};
+  const pointer = typeInfo?.members.find(m => m.name === '*');
+  const linearMemorySize = pointer ? sourceType.typeMap.get(pointer.typeId)?.typeInfo?.size : typeInfo?.size;
+  const instance = sinon.createStubInstance(Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject);
+
+  sinon.stub(instance, 'linearMemoryAddress').get(() => inspectableAddress);
+  sinon.stub(instance, 'linearMemorySize').get(() => linearMemorySize);
+  sinon.stub(instance, 'description').get(() => description);
+
+  return instance;
+}
+
 describeWithEnvironment('LinearMemoryInspectorController', () => {
   it('throws an error on an invalid (out-of-bounds) memory range request', async () => {
     const array = new Uint8Array([2, 4, 6, 2, 4]);
@@ -119,8 +133,15 @@
       },
     } as Bindings.DebuggerLanguagePlugins.ValueNode;
 
-    const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(mockValueNode);
-    assert.strictEqual(size, expectedSize);
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(mockValueNode);
+      assert.strictEqual(size, expectedSize);
+    }
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(
+          createFakeExtensionRemoteObjectFromValueNode(mockValueNode));
+      assert.strictEqual(size, expectedSize);
+    }
   });
 
   it('retrieves object size for a pointer ValueNode', () => {
@@ -146,8 +167,15 @@
       },
     } as Bindings.DebuggerLanguagePlugins.ValueNode;
 
-    const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(pointerValueNode);
-    assert.strictEqual(size, expectedSize);
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(pointerValueNode);
+      assert.strictEqual(size, expectedSize);
+    }
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(
+          createFakeExtensionRemoteObjectFromValueNode(pointerValueNode));
+      assert.strictEqual(size, expectedSize);
+    }
   });
 
   it('retrieves pointer size for a pointer-to-pointer ValueNode', () => {
@@ -183,8 +211,15 @@
       },
     } as Bindings.DebuggerLanguagePlugins.ValueNode;
 
-    const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(pointerValueNode);
-    assert.strictEqual(size, expectedSize);
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(pointerValueNode);
+      assert.strictEqual(size, expectedSize);
+    }
+    {
+      const size = LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectSize(
+          createFakeExtensionRemoteObjectFromValueNode(pointerValueNode));
+      assert.strictEqual(size, expectedSize);
+    }
   });
 
   it('throws an error when retrieving size of non-conforming (multiple pointer members) ValueNode', () => {
@@ -299,30 +334,58 @@
 
   it('extracts array type correctly', () => {
     const obj = {description: 'int[]'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
-                              .extractObjectTypeDescription(obj);
-    assert.strictEqual(extractedType, 'int[]');
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(obj);
+      assert.strictEqual(extractedType, 'int[]');
+    }
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(createFakeExtensionRemoteObjectFromValueNode(obj));
+      assert.strictEqual(extractedType, 'int[]');
+    }
   });
 
   it('extracts multi-level pointer correctly', () => {
     const obj = {description: 'int **'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
-                              .extractObjectTypeDescription(obj);
-    assert.strictEqual(extractedType, 'int *');
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(obj);
+      assert.strictEqual(extractedType, 'int *');
+    }
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(createFakeExtensionRemoteObjectFromValueNode(obj));
+      assert.strictEqual(extractedType, 'int *');
+    }
   });
 
   it('extracts reference type correctly', () => {
     const obj = {description: 'int &'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
-                              .extractObjectTypeDescription(obj);
-    assert.strictEqual(extractedType, 'int');
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(obj);
+      assert.strictEqual(extractedType, 'int');
+    }
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(createFakeExtensionRemoteObjectFromValueNode(obj));
+      assert.strictEqual(extractedType, 'int');
+    }
   });
 
   it('extracts pointer type correctly', () => {
     const obj = {description: 'int *'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
-                              .extractObjectTypeDescription(obj);
-    assert.strictEqual(extractedType, 'int');
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(obj);
+      assert.strictEqual(extractedType, 'int');
+    }
+    {
+      const extractedType = LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController
+                                .extractObjectTypeDescription(createFakeExtensionRemoteObjectFromValueNode(obj));
+      assert.strictEqual(extractedType, 'int');
+    }
   });
 
   it('removes the provided highlightInfo when it is stored in the Controller', () => {
@@ -356,19 +419,35 @@
   it('extracts name unchanged when object is not pointer', () => {
     const name = 'myNumbers';
     const obj = {description: 'int[]'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedName =
-        LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
-            obj, name);
-    assert.strictEqual(extractedName, name);
+    {
+      const extractedName =
+          LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
+              obj, name);
+      assert.strictEqual(extractedName, name);
+    }
+    {
+      const extractedName =
+          LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
+              createFakeExtensionRemoteObjectFromValueNode(obj), name);
+      assert.strictEqual(extractedName, name);
+    }
   });
 
   it('extracts name with preprended \'*\' when object is a pointer', () => {
     const name = 'myPointerObject';
     const obj = {description: 'int *'} as Bindings.DebuggerLanguagePlugins.ValueNode;
-    const extractedName =
-        LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
-            obj, name);
-    assert.strictEqual(extractedName, '*' + name);
+    {
+      const extractedName =
+          LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
+              obj, name);
+      assert.strictEqual(extractedName, '*' + name);
+    }
+    {
+      const extractedName =
+          LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.extractObjectName(
+              createFakeExtensionRemoteObjectFromValueNode(obj), name);
+      assert.strictEqual(extractedName, '*' + name);
+    }
   });
 });
 
diff --git a/test/unittests/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection_test.ts b/test/unittests/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection_test.ts
index 504c2bd..fb27005 100644
--- a/test/unittests/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection_test.ts
+++ b/test/unittests/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection_test.ts
@@ -15,6 +15,8 @@
 import {assertNotNullOrUndefined} from '../../../../../../../front_end/core/platform/platform.js';
 import {createTarget} from '../../../../helpers/EnvironmentHelpers.js';
 import {describeWithMockConnection} from '../../../../helpers/MockConnection.js';
+import {type Chrome} from '../../../../../../../extension-api/ExtensionAPI.js';
+import {TestPlugin} from '../../../../helpers/LanguagePluginHelpers.js';
 
 describeWithRealConnection('ObjectPropertiesSection', () => {
   async function setupTreeOutline(
@@ -251,14 +253,34 @@
     const callFrame = {
       debuggerModel,
     } as SDK.DebuggerModel.CallFrame;
-    const valueNode = new Bindings.DebuggerLanguagePlugins.ValueNode(
-        callFrame, undefined, 'object', undefined, undefined, 2 /* inspectableAddress*/);
+    {
+      const valueNode = new Bindings.DebuggerLanguagePlugins.ValueNode(
+          callFrame, undefined, 'object', undefined, undefined, 2 /* inspectableAddress*/);
 
-    const div = document.createElement('div');
-    assert.isFalse(div.hasChildNodes());
-    ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, valueNode);
-    assert.isTrue(div.hasChildNodes());
-    const icon = div.getElementsByClassName('devtools-icon');
-    assert.isNotNull(icon);
+      const div = document.createElement('div');
+      assert.isFalse(div.hasChildNodes());
+      ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, valueNode);
+      assert.isTrue(div.hasChildNodes());
+      const icon = div.getElementsByClassName('devtools-icon');
+      assert.isNotNull(icon);
+    }
+    {
+      const extensionObject = {
+        type: 'string' as Chrome.DevTools.RemoteObjectType,
+        hasChildren: false,
+        description: 'hello',
+        linearMemoryAddress: 2,
+      };
+      const plugin = new TestPlugin('LinearMemoryInspectorTestPlugin');
+      const remoteObject =
+          new Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject(callFrame, extensionObject, plugin);
+
+      const div = document.createElement('div');
+      assert.isFalse(div.hasChildNodes());
+      ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, remoteObject);
+      assert.isTrue(div.hasChildNodes());
+      const icon = div.getElementsByClassName('devtools-icon');
+      assert.isNotNull(icon);
+    }
   });
 });