Commit f8ffab5d authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

DevTools: generalize preview without side effect builder

Introduces JavaScriptREPL, a utility that includes
- Logic used by EagerEval for building side-effect-free previews
- Console prompt's preprocessing logic
  (object literal wrapping and top level await)

Bug: 849875
Change-Id: Ie8146435fced9ebf440b43c225d25ef145416247
Reviewed-on: https://chromium-review.googlesource.com/1145911Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Commit-Queue: Erik Luo <luoe@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577341}
parent 7468905c
Tests $x for iterator and non-iterator types.
$x('42')
$x('name(/html)')
$x('not(42)')
$x('/html/body/p').length
$x('//a/@href')[0]
$x('./a/@href', document.body)[0]
$x('./a@href', document.body)
42
$x('name(/html)')
"html"
$x('not(42)')
false
$x('/html/body/p').length
1
$x('//a/@href')[0]
href="http://chromium.org"
$x('./a/@href', document.body)[0]
href="http://chromium.org"
$x('./a@href', document.body)
VM:1 Uncaught DOMException: Failed to execute '$x' on 'CommandLineAPI': The string './a@href' is not a valid XPath expression.
at <anonymous>:1:1
(anonymous) @ VM:1
......
......@@ -15,13 +15,13 @@
Console.ConsoleViewMessage.prototype, '_formattedParameterAsNodeForTest', formattedParameter, true);
ConsoleTestRunner.addConsoleViewSniffer(messageSniffer, true);
ConsoleTestRunner.evaluateInConsole('$x(\'42\')'); // number
ConsoleTestRunner.evaluateInConsole('$x(\'name(/html)\')'); // string
ConsoleTestRunner.evaluateInConsole('$x(\'not(42)\')'); // boolean
ConsoleTestRunner.evaluateInConsole('$x(\'/html/body/p\').length'); // node iterator
ConsoleTestRunner.evaluateInConsole('$x(\'//a/@href\')[0]'); // href, should not throw
ConsoleTestRunner.evaluateInConsole('$x(\'./a/@href\', document.body)[0]'); // relative to document.body selector
ConsoleTestRunner.evaluateInConsole('$x(\'./a@href\', document.body)'); // incorrect selector, shouldn't crash
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'42\')'); // number
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'name(/html)\')'); // string
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'not(42)\')'); // boolean
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'/html/body/p\').length'); // node iterator
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'//a/@href\')[0]'); // href, should not throw
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'./a/@href\', document.body)[0]'); // relative to document.body selector
await ConsoleTestRunner.evaluateInConsolePromise('$x(\'./a@href\', document.body)'); // incorrect selector, shouldn't crash
TestRunner.evaluateInPage('console.log(\'complete\')'); // node iterator
var completeMessageReceived = false;
......
......@@ -32,10 +32,10 @@
async function testNoOpForLongText(next) {
TestRunner.addResult('Setting max length for evaluation to 0');
const originalMaxLength = Console.ConsolePrompt._MaxLengthForEvaluation;
Console.ConsolePrompt._MaxLengthForEvaluation = 0;
const originalMaxLength = ObjectUI.JavaScriptREPL._MaxLengthForEvaluation;
ObjectUI.JavaScriptREPL._MaxLengthForEvaluation = 0;
await checkExpression(`1 + 2`);
Console.ConsolePrompt._MaxLengthForEvaluation = originalMaxLength;
ObjectUI.JavaScriptREPL._MaxLengthForEvaluation = originalMaxLength;
next();
},
......
......@@ -50,18 +50,18 @@
}
]);
function setBreakpointAndRun(next, functionName, runCmd) {
ConsoleTestRunner.evaluateInConsole('debug(' + functionName + ')');
async function setBreakpointAndRun(next, functionName, runCmd) {
await ConsoleTestRunner.evaluateInConsolePromise('debug(' + functionName + ')');
TestRunner.addResult('Breakpoint added.');
ConsoleTestRunner.evaluateInConsole('setTimeout(function() { ' + runCmd + ' }, 0)');
await ConsoleTestRunner.evaluateInConsolePromise('setTimeout(function() { ' + runCmd + ' }, 20)');
TestRunner.addResult('Set timer for test function.');
SourcesTestRunner.waitUntilPaused(didPause);
function didPause(callFrames, reason) {
async function didPause(callFrames, reason) {
TestRunner.addResult('Script execution paused.');
SourcesTestRunner.captureStackTrace(callFrames);
ConsoleTestRunner.evaluateInConsole('undebug(' + functionName + ')');
await ConsoleTestRunner.evaluateInConsolePromise('undebug(' + functionName + ')');
TestRunner.addResult('Breakpoint removed.');
TestRunner.assertEquals(reason, SDK.DebuggerModel.BreakReason.DebugCommand);
SourcesTestRunner.resumeExecution(didResume);
......
......@@ -449,6 +449,7 @@ all_devtools_files = [
"front_end/object_ui/customPreviewComponent.css",
"front_end/object_ui/CustomPreviewComponent.js",
"front_end/object_ui/JavaScriptAutocomplete.js",
"front_end/object_ui/JavaScriptREPL.js",
"front_end/object_ui/module.json",
"front_end/object_ui/objectPopover.css",
"front_end/object_ui/ObjectPopoverHelper.js",
......
......@@ -104,40 +104,10 @@ Console.ConsolePrompt = class extends UI.Widget {
*/
async _requestPreview() {
const text = this._editor.textWithCurrentSuggestion().trim();
const executionContext = UI.context.flavor(SDK.ExecutionContext);
if (!executionContext || !text || text.length > Console.ConsolePrompt._MaxLengthForEvaluation) {
this._innerPreviewElement.removeChildren();
return;
}
const options = {
expression: SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text),
includeCommandLineAPI: true,
generatePreview: true,
throwOnSideEffect: true,
timeout: 500
};
const result = await executionContext.evaluate(options, true /* userGesture */, false /* awaitPromise */);
const {preview} = await ObjectUI.JavaScriptREPL.evaluateAndBuildPreview(text, true /* throwOnSideEffect */, 500);
this._innerPreviewElement.removeChildren();
if (result.error)
return;
if (result.exceptionDetails) {
const exception = result.exceptionDetails.exception.description;
if (exception.startsWith('TypeError: '))
this._innerPreviewElement.textContent = exception;
return;
}
const {preview, type, subtype, description} = result.object;
if (preview && type === 'object' && subtype !== 'node') {
this._formatter.appendObjectPreview(this._innerPreviewElement, preview, false /* isEntry */);
} else {
const nonObjectPreview = this._formatter.renderPropertyPreview(type, subtype, description.trimEnd(400));
this._innerPreviewElement.appendChild(nonObjectPreview);
}
if (this._innerPreviewElement.deepTextContent() === this._editor.textWithCurrentSuggestion().trim())
this._innerPreviewElement.removeChildren();
if (preview.deepTextContent() !== this._editor.textWithCurrentSuggestion().trim())
this._innerPreviewElement.appendChild(preview);
}
/**
......@@ -292,15 +262,10 @@ Console.ConsolePrompt = class extends UI.Widget {
if (currentExecutionContext) {
const executionContext = currentExecutionContext;
const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text);
let preprocessed = false;
if (text.indexOf('await') !== -1) {
const preprocessedText = await Formatter.formatterWorkerPool().preprocessTopLevelAwaitExpressions(text);
preprocessed = !!preprocessedText;
text = preprocessedText || text;
}
const wrappedResult = await ObjectUI.JavaScriptREPL.preprocessExpression(text);
SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text, useCommandLineAPI, /* awaitPromise */ preprocessed);
executionContext, message, wrappedResult.text, useCommandLineAPI,
/* awaitPromise */ wrappedResult.preprocessed);
if (Console.ConsolePanel.instance().isShowing())
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel);
}
......@@ -361,12 +326,6 @@ Console.ConsolePrompt = class extends UI.Widget {
}
};
/**
* @const
* @type {number}
*/
Console.ConsolePrompt._MaxLengthForEvaluation = 2000;
/**
* @unrestricted
*/
......
// Copyright 2018 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.
ObjectUI.JavaScriptREPL = class {
/**
* @param {string} code
* @return {string}
*/
static wrapObjectLiteral(code) {
// Only parenthesize what appears to be an object literal.
if (!(/^\s*\{/.test(code) && /\}\s*$/.test(code)))
return code;
const parse = (async () => 0).constructor;
try {
// Check if the code can be interpreted as an expression.
parse('return ' + code + ';');
// No syntax error! Does it work parenthesized?
const wrappedCode = '(' + code + ')';
parse(wrappedCode);
return wrappedCode;
} catch (e) {
return code;
}
}
/**
* @param {string} text
* @return {!Promise<!{text: string, preprocessed: boolean}>}
*/
static async preprocessExpression(text) {
text = ObjectUI.JavaScriptREPL.wrapObjectLiteral(text);
let preprocessed = false;
if (text.indexOf('await') !== -1) {
const preprocessedText = await Formatter.formatterWorkerPool().preprocessTopLevelAwaitExpressions(text);
preprocessed = !!preprocessedText;
text = preprocessedText || text;
}
return {text, preprocessed};
}
/**
* @param {string} text
* @param {boolean} throwOnSideEffect
* @param {number=} timeout
* @return {!Promise<!{preview: !DocumentFragment, result: ?SDK.RuntimeModel.EvaluationResult}>}
*/
static async evaluateAndBuildPreview(text, throwOnSideEffect, timeout) {
const executionContext = UI.context.flavor(SDK.ExecutionContext);
const isTextLong = text.length > ObjectUI.JavaScriptREPL._MaxLengthForEvaluation;
if (!text || !executionContext || (throwOnSideEffect && isTextLong))
return {preview: createDocumentFragment(), result: null};
const wrappedResult = await ObjectUI.JavaScriptREPL.preprocessExpression(text);
const options = {
expression: wrappedResult.text,
generatePreview: true,
includeCommandLineAPI: true,
throwOnSideEffect: throwOnSideEffect,
timeout: timeout
};
const result = await executionContext.evaluate(
options, false /* userGesture */, wrappedResult.preprocessed /* awaitPromise */);
const preview = ObjectUI.JavaScriptREPL._buildEvaluationPreview(result);
return {preview, result};
}
/**
* @param {!SDK.RuntimeModel.EvaluationResult} result
* @return {!DocumentFragment}
*/
static _buildEvaluationPreview(result) {
const fragment = createDocumentFragment();
if (result.error)
return fragment;
if (result.exceptionDetails) {
const exception = result.exceptionDetails.exception.description;
if (exception.startsWith('TypeError: '))
fragment.createChild('span').textContent = exception;
return fragment;
}
const formatter = new ObjectUI.RemoteObjectPreviewFormatter();
const {preview, type, subtype, description} = result.object;
if (preview && type === 'object' && subtype !== 'node') {
formatter.appendObjectPreview(fragment, preview, false /* isEntry */);
} else {
const nonObjectPreview = formatter.renderPropertyPreview(type, subtype, description.trimEnd(400));
fragment.appendChild(nonObjectPreview);
}
return fragment;
}
};
/**
* @const
* @type {number}
*/
ObjectUI.JavaScriptREPL._MaxLengthForEvaluation = 2000;
......@@ -918,7 +918,7 @@ ObjectUI.ObjectPropertyTreeElement = class extends UI.TreeElement {
*/
async _applyExpression(expression) {
const property = SDK.RemoteObject.toCallArgument(this.property.symbol || this.property.name);
expression = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(expression.trim());
expression = ObjectUI.JavaScriptREPL.wrapObjectLiteral(expression.trim());
if (this.property.synthetic) {
let invalidate = false;
......
......@@ -32,7 +32,7 @@ ObjectUI.RemoteObjectPreviewFormatter = class {
}
/**
* @param {!Element} parentElement
* @param {!DocumentFragment|!Element} parentElement
* @param {!Protocol.Runtime.ObjectPreview} preview
* @param {boolean} isEntry
*/
......
......@@ -19,6 +19,7 @@
"ObjectPopoverHelper.js",
"ObjectPropertiesSection.js",
"JavaScriptAutocomplete.js",
"JavaScriptREPL.js",
"RemoteObjectPreviewFormatter.js"
],
"resources": [
......
......@@ -53,30 +53,6 @@ SDK.RuntimeModel = class extends SDK.SDKModel {
Common.moduleSetting('customFormatters').addChangeListener(this._customFormattersStateChanged.bind(this));
}
/**
* @param {string} code
* @return {string}
*/
static wrapObjectLiteralExpressionIfNeeded(code) {
// Only parenthesize what appears to be an object literal.
if (!(/^\s*\{/.test(code) && /\}\s*$/.test(code)))
return code;
const parse = (async () => 0).constructor;
try {
// Check if the code can be interpreted as an expression.
parse('return ' + code + ';');
// No syntax error! Does it work parenthesized?
const wrappedCode = '(' + code + ')';
parse(wrappedCode);
return wrappedCode;
} catch (e) {
return code;
}
}
/**
* @return {!SDK.DebuggerModel}
*/
......
......@@ -868,7 +868,7 @@ Sources.SourcesPanel = class extends UI.Panel {
const executionContext = /** @type {!SDK.ExecutionContext} */ (currentExecutionContext);
let text = /** @type {string} */ (callFunctionResult.object.value);
const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text);
text = ObjectUI.JavaScriptREPL.wrapObjectLiteral(text);
SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text,
/* useCommandLineAPI */ false, /* awaitPromise */ false);
......@@ -1202,7 +1202,7 @@ Sources.SourcesPanel.DebuggingActionDelegate = class {
const executionContext = UI.context.flavor(SDK.ExecutionContext);
if (executionContext) {
const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text);
text = ObjectUI.JavaScriptREPL.wrapObjectLiteral(text);
SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text, /* useCommandLineAPI */ true, /* awaitPromise */ false);
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment