v8/test/inspector/private-class-member-inspector-test.js
Joyee Cheung 4d0d31f41b [class] support out-of-scope private member access in debug-evaluate
Previously in the DevTools console, users could inspect a preview of all private class members on an instance, but if they wanted to evaluate or inspect a  specific private class member out of a long list, they had to be debugging and in a scope that has access to those private names.

This patch adds support for extraordinary access of out-of-scope private member access in debug-evaluate, specifically for Debugger.evaluateOnCallframe() (for console calls invoked during debugging) and Runtime.evaluate() (for console calls invoked when the user is not debugging). This kind of access is not otherwise allowed in normal execution, but in the DevTools console it makes sense to relax the rules a bit for a better developer experience.

To support this kind of extraordinary access, if the parsing_while_debugging or is_repl_mode flag is set, when we encounter a private name reference that's in a top-level scope or an eval scope under a top-level scope, instead of throwing immediately, we bind the reference to a dynamic lookup variable, and emit bytecode that calls to %GetPrivateName() or %SetPrivateName() in the runtime to perform lookup of the private name as well as the load/store operations accordingly.

If there are more than on private name on the receiver matching the description (for example, an object with two `#field` private names from different classes), we throw an error for the ambiguity (we can consider supporting selection among the conflicting private names later, for the initial support we just throw for simplicity).

If there are no matching private names, or if the found private class member does not support the desired operation (e.g. attempting to write to a read-only private accessor), we throw an error as well.

If there is exactly one matching private name, and the found private class member support the desired operation, we dispatch to the proper behavior in the runtime calls.

Doc: https://docs.google.com/document/d/1Va89BKHjCDs9RccDWhuZBb6LyRMAd6BXM3-p25oHd8I/edit

Bug: chromium:1381806
Change-Id: I7d1db709470246050d2e4c2a85b2292e63c01fe9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4020267
Commit-Queue: Joyee Cheung <joyee@igalia.com>
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/main@{#85421}
2023-01-20 22:26:44 +00:00

168 lines
5.2 KiB
JavaScript

// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
PrivateClassMemberInspectorTest = {};
function getTestReceiver(type) {
return type === 'private-instance-member' ? 'obj' : 'Klass';
}
function getSetupScript({ type, testRuntime }) {
const pause = testRuntime ? '' : 'debugger;';
if (type === 'private-instance-member' || type === 'private-static-member') {
const isStatic = type === 'private-static-member';
const prefix = isStatic ? 'static' : '';
return `
class Klass {
${prefix} #field = "string";
${prefix} get #getterOnly() { return "getterOnly"; }
${prefix} set #setterOnly(val) { this.#field = "setterOnlyCalled"; }
${prefix} get #accessor() { return this.#field }
${prefix} set #accessor(val) { this.#field = val; }
${prefix} #method() { return "method"; }
}
const obj = new Klass();
${pause}`;
}
if (type !== 'private-conflicting-member') {
throw new Error('unknown test type');
}
return `
class Klass {
#name = "string";
}
class ClassWithField extends Klass {
#name = "child";
}
class ClassWithMethod extends Klass {
#name() {}
}
class ClassWithAccessor extends Klass {
get #name() {}
set #name(val) {}
}
class StaticClass extends Klass {
static #name = "child";
}
${pause}`;
}
async function testAllPrivateMembers(type, runAndLog) {
const receiver = getTestReceiver(type);
InspectorTest.log('Checking private fields');
await runAndLog(`${receiver}.#field`);
await runAndLog(`${receiver}.#field = 1`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`${receiver}.#field++`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`++${receiver}.#field`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`${receiver}.#field -= 3`);
await runAndLog(`${receiver}.#field`);
InspectorTest.log('Checking private getter-only accessors');
await runAndLog(`${receiver}.#getterOnly`);
await runAndLog(`${receiver}.#getterOnly = 1`);
await runAndLog(`${receiver}.#getterOnly++`);
await runAndLog(`${receiver}.#getterOnly -= 3`);
await runAndLog(`${receiver}.#getterOnly`);
InspectorTest.log('Checking private setter-only accessors');
await runAndLog(`${receiver}.#setterOnly`);
await runAndLog(`${receiver}.#setterOnly = 1`);
await runAndLog(`${receiver}.#setterOnly++`);
await runAndLog(`${receiver}.#setterOnly -= 3`);
await runAndLog(`${receiver}.#field`);
InspectorTest.log('Checking private accessors');
await runAndLog(`${receiver}.#accessor`);
await runAndLog(`${receiver}.#accessor = 1`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`${receiver}.#accessor++`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`++${receiver}.#accessor`);
await runAndLog(`${receiver}.#field`);
await runAndLog(`${receiver}.#accessor -= 3`);
await runAndLog(`${receiver}.#field`);
InspectorTest.log('Checking private methods');
await runAndLog(`${receiver}.#method`);
await runAndLog(`${receiver}.#method = 1`);
await runAndLog(`${receiver}.#method++`);
await runAndLog(`++${receiver}.#method`);
await runAndLog(`${receiver}.#method -= 3`);
}
async function testConflictingPrivateMembers(runAndLog) {
await runAndLog(`(new ClassWithField).#name`);
await runAndLog(`(new ClassWithMethod).#name`);
await runAndLog(`(new ClassWithAccessor).#name`);
await runAndLog(`StaticClass.#name`);
await runAndLog(`(new StaticClass).#name`);
}
async function runPrivateClassMemberTest(Protocol, { type, testRuntime }) {
let runAndLog;
if (testRuntime) {
runAndLog = async function runAndLog(expression) {
InspectorTest.log(`Runtime.evaluate: \`${expression}\``);
const { result: { result } } =
await Protocol.Runtime.evaluate({ expression, replMode: true });
InspectorTest.logMessage(result);
}
} else {
const { params: { callFrames } } = await Protocol.Debugger.oncePaused();
const frame = callFrames[0];
runAndLog = async function runAndLog(expression) {
InspectorTest.log(`Debugger.evaluateOnCallFrame: \`${expression}\``);
const { result: { result } } =
await Protocol.Debugger.evaluateOnCallFrame({
callFrameId: frame.callFrameId,
expression
});
InspectorTest.logMessage(result);
}
}
switch (type) {
case 'private-instance-member':
case 'private-static-member': {
await testAllPrivateMembers(type, runAndLog);
break;
}
case 'private-conflicting-member': {
await testConflictingPrivateMembers(runAndLog);
break;
}
default:
throw new Error('unknown test type');
}
await Protocol.Debugger.resume();
}
PrivateClassMemberInspectorTest.runTest = function (InspectorTest, options) {
const { contextGroup, Protocol } = InspectorTest.start(options.message);
if (options.testRuntime) {
Protocol.Runtime.enable();
} else {
Protocol.Debugger.enable();
}
const source = getSetupScript(options);
InspectorTest.log(source);
if (options.module) {
contextGroup.addModule(source, 'module');
} else {
contextGroup.addScript(source);
}
InspectorTest.runAsyncTestSuite([async function evaluatePrivateMembers() {
await runPrivateClassMemberTest(Protocol, options);
}]);
}