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. Tests $x for iterator and non-iterator types.
$x('42') $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 42
$x('name(/html)')
"html" "html"
$x('not(42)')
false false
$x('/html/body/p').length
1 1
$x('//a/@href')[0]
href="http://chromium.org" href="http://chromium.org"
$x('./a/@href', document.body)[0]
href="http://chromium.org" 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. VM:1 Uncaught DOMException: Failed to execute '$x' on 'CommandLineAPI': The string './a@href' is not a valid XPath expression.
at <anonymous>:1:1 at <anonymous>:1:1
(anonymous) @ VM:1 (anonymous) @ VM:1
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
Console.ConsoleViewMessage.prototype, '_formattedParameterAsNodeForTest', formattedParameter, true); Console.ConsoleViewMessage.prototype, '_formattedParameterAsNodeForTest', formattedParameter, true);
ConsoleTestRunner.addConsoleViewSniffer(messageSniffer, true); ConsoleTestRunner.addConsoleViewSniffer(messageSniffer, true);
ConsoleTestRunner.evaluateInConsole('$x(\'42\')'); // number await ConsoleTestRunner.evaluateInConsolePromise('$x(\'42\')'); // number
ConsoleTestRunner.evaluateInConsole('$x(\'name(/html)\')'); // string await ConsoleTestRunner.evaluateInConsolePromise('$x(\'name(/html)\')'); // string
ConsoleTestRunner.evaluateInConsole('$x(\'not(42)\')'); // boolean await ConsoleTestRunner.evaluateInConsolePromise('$x(\'not(42)\')'); // boolean
ConsoleTestRunner.evaluateInConsole('$x(\'/html/body/p\').length'); // node iterator await ConsoleTestRunner.evaluateInConsolePromise('$x(\'/html/body/p\').length'); // node iterator
ConsoleTestRunner.evaluateInConsole('$x(\'//a/@href\')[0]'); // href, should not throw await ConsoleTestRunner.evaluateInConsolePromise('$x(\'//a/@href\')[0]'); // href, should not throw
ConsoleTestRunner.evaluateInConsole('$x(\'./a/@href\', document.body)[0]'); // relative to document.body selector await ConsoleTestRunner.evaluateInConsolePromise('$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(\'./a@href\', document.body)'); // incorrect selector, shouldn't crash
TestRunner.evaluateInPage('console.log(\'complete\')'); // node iterator TestRunner.evaluateInPage('console.log(\'complete\')'); // node iterator
var completeMessageReceived = false; var completeMessageReceived = false;
......
...@@ -32,10 +32,10 @@ ...@@ -32,10 +32,10 @@
async function testNoOpForLongText(next) { async function testNoOpForLongText(next) {
TestRunner.addResult('Setting max length for evaluation to 0'); TestRunner.addResult('Setting max length for evaluation to 0');
const originalMaxLength = Console.ConsolePrompt._MaxLengthForEvaluation; const originalMaxLength = ObjectUI.JavaScriptREPL._MaxLengthForEvaluation;
Console.ConsolePrompt._MaxLengthForEvaluation = 0; ObjectUI.JavaScriptREPL._MaxLengthForEvaluation = 0;
await checkExpression(`1 + 2`); await checkExpression(`1 + 2`);
Console.ConsolePrompt._MaxLengthForEvaluation = originalMaxLength; ObjectUI.JavaScriptREPL._MaxLengthForEvaluation = originalMaxLength;
next(); next();
}, },
......
...@@ -50,18 +50,18 @@ ...@@ -50,18 +50,18 @@
} }
]); ]);
function setBreakpointAndRun(next, functionName, runCmd) { async function setBreakpointAndRun(next, functionName, runCmd) {
ConsoleTestRunner.evaluateInConsole('debug(' + functionName + ')'); await ConsoleTestRunner.evaluateInConsolePromise('debug(' + functionName + ')');
TestRunner.addResult('Breakpoint added.'); TestRunner.addResult('Breakpoint added.');
ConsoleTestRunner.evaluateInConsole('setTimeout(function() { ' + runCmd + ' }, 0)'); await ConsoleTestRunner.evaluateInConsolePromise('setTimeout(function() { ' + runCmd + ' }, 20)');
TestRunner.addResult('Set timer for test function.'); TestRunner.addResult('Set timer for test function.');
SourcesTestRunner.waitUntilPaused(didPause); SourcesTestRunner.waitUntilPaused(didPause);
function didPause(callFrames, reason) { async function didPause(callFrames, reason) {
TestRunner.addResult('Script execution paused.'); TestRunner.addResult('Script execution paused.');
SourcesTestRunner.captureStackTrace(callFrames); SourcesTestRunner.captureStackTrace(callFrames);
ConsoleTestRunner.evaluateInConsole('undebug(' + functionName + ')'); await ConsoleTestRunner.evaluateInConsolePromise('undebug(' + functionName + ')');
TestRunner.addResult('Breakpoint removed.'); TestRunner.addResult('Breakpoint removed.');
TestRunner.assertEquals(reason, SDK.DebuggerModel.BreakReason.DebugCommand); TestRunner.assertEquals(reason, SDK.DebuggerModel.BreakReason.DebugCommand);
SourcesTestRunner.resumeExecution(didResume); SourcesTestRunner.resumeExecution(didResume);
......
...@@ -449,6 +449,7 @@ all_devtools_files = [ ...@@ -449,6 +449,7 @@ all_devtools_files = [
"front_end/object_ui/customPreviewComponent.css", "front_end/object_ui/customPreviewComponent.css",
"front_end/object_ui/CustomPreviewComponent.js", "front_end/object_ui/CustomPreviewComponent.js",
"front_end/object_ui/JavaScriptAutocomplete.js", "front_end/object_ui/JavaScriptAutocomplete.js",
"front_end/object_ui/JavaScriptREPL.js",
"front_end/object_ui/module.json", "front_end/object_ui/module.json",
"front_end/object_ui/objectPopover.css", "front_end/object_ui/objectPopover.css",
"front_end/object_ui/ObjectPopoverHelper.js", "front_end/object_ui/ObjectPopoverHelper.js",
......
...@@ -104,40 +104,10 @@ Console.ConsolePrompt = class extends UI.Widget { ...@@ -104,40 +104,10 @@ Console.ConsolePrompt = class extends UI.Widget {
*/ */
async _requestPreview() { async _requestPreview() {
const text = this._editor.textWithCurrentSuggestion().trim(); const text = this._editor.textWithCurrentSuggestion().trim();
const executionContext = UI.context.flavor(SDK.ExecutionContext); const {preview} = await ObjectUI.JavaScriptREPL.evaluateAndBuildPreview(text, true /* throwOnSideEffect */, 500);
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 */);
this._innerPreviewElement.removeChildren(); this._innerPreviewElement.removeChildren();
if (result.error) if (preview.deepTextContent() !== this._editor.textWithCurrentSuggestion().trim())
return; this._innerPreviewElement.appendChild(preview);
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();
} }
/** /**
...@@ -292,15 +262,10 @@ Console.ConsolePrompt = class extends UI.Widget { ...@@ -292,15 +262,10 @@ Console.ConsolePrompt = class extends UI.Widget {
if (currentExecutionContext) { if (currentExecutionContext) {
const executionContext = currentExecutionContext; const executionContext = currentExecutionContext;
const message = SDK.consoleModel.addCommandMessage(executionContext, text); const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text); const wrappedResult = await ObjectUI.JavaScriptREPL.preprocessExpression(text);
let preprocessed = false;
if (text.indexOf('await') !== -1) {
const preprocessedText = await Formatter.formatterWorkerPool().preprocessTopLevelAwaitExpressions(text);
preprocessed = !!preprocessedText;
text = preprocessedText || text;
}
SDK.consoleModel.evaluateCommandInConsole( SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text, useCommandLineAPI, /* awaitPromise */ preprocessed); executionContext, message, wrappedResult.text, useCommandLineAPI,
/* awaitPromise */ wrappedResult.preprocessed);
if (Console.ConsolePanel.instance().isShowing()) if (Console.ConsolePanel.instance().isShowing())
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel); Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel);
} }
...@@ -361,12 +326,6 @@ Console.ConsolePrompt = class extends UI.Widget { ...@@ -361,12 +326,6 @@ Console.ConsolePrompt = class extends UI.Widget {
} }
}; };
/**
* @const
* @type {number}
*/
Console.ConsolePrompt._MaxLengthForEvaluation = 2000;
/** /**
* @unrestricted * @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 { ...@@ -918,7 +918,7 @@ ObjectUI.ObjectPropertyTreeElement = class extends UI.TreeElement {
*/ */
async _applyExpression(expression) { async _applyExpression(expression) {
const property = SDK.RemoteObject.toCallArgument(this.property.symbol || this.property.name); 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) { if (this.property.synthetic) {
let invalidate = false; let invalidate = false;
......
...@@ -32,7 +32,7 @@ ObjectUI.RemoteObjectPreviewFormatter = class { ...@@ -32,7 +32,7 @@ ObjectUI.RemoteObjectPreviewFormatter = class {
} }
/** /**
* @param {!Element} parentElement * @param {!DocumentFragment|!Element} parentElement
* @param {!Protocol.Runtime.ObjectPreview} preview * @param {!Protocol.Runtime.ObjectPreview} preview
* @param {boolean} isEntry * @param {boolean} isEntry
*/ */
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"ObjectPopoverHelper.js", "ObjectPopoverHelper.js",
"ObjectPropertiesSection.js", "ObjectPropertiesSection.js",
"JavaScriptAutocomplete.js", "JavaScriptAutocomplete.js",
"JavaScriptREPL.js",
"RemoteObjectPreviewFormatter.js" "RemoteObjectPreviewFormatter.js"
], ],
"resources": [ "resources": [
......
...@@ -53,30 +53,6 @@ SDK.RuntimeModel = class extends SDK.SDKModel { ...@@ -53,30 +53,6 @@ SDK.RuntimeModel = class extends SDK.SDKModel {
Common.moduleSetting('customFormatters').addChangeListener(this._customFormattersStateChanged.bind(this)); 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} * @return {!SDK.DebuggerModel}
*/ */
......
...@@ -868,7 +868,7 @@ Sources.SourcesPanel = class extends UI.Panel { ...@@ -868,7 +868,7 @@ Sources.SourcesPanel = class extends UI.Panel {
const executionContext = /** @type {!SDK.ExecutionContext} */ (currentExecutionContext); const executionContext = /** @type {!SDK.ExecutionContext} */ (currentExecutionContext);
let text = /** @type {string} */ (callFunctionResult.object.value); let text = /** @type {string} */ (callFunctionResult.object.value);
const message = SDK.consoleModel.addCommandMessage(executionContext, text); const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text); text = ObjectUI.JavaScriptREPL.wrapObjectLiteral(text);
SDK.consoleModel.evaluateCommandInConsole( SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text, executionContext, message, text,
/* useCommandLineAPI */ false, /* awaitPromise */ false); /* useCommandLineAPI */ false, /* awaitPromise */ false);
...@@ -1202,7 +1202,7 @@ Sources.SourcesPanel.DebuggingActionDelegate = class { ...@@ -1202,7 +1202,7 @@ Sources.SourcesPanel.DebuggingActionDelegate = class {
const executionContext = UI.context.flavor(SDK.ExecutionContext); const executionContext = UI.context.flavor(SDK.ExecutionContext);
if (executionContext) { if (executionContext) {
const message = SDK.consoleModel.addCommandMessage(executionContext, text); const message = SDK.consoleModel.addCommandMessage(executionContext, text);
text = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(text); text = ObjectUI.JavaScriptREPL.wrapObjectLiteral(text);
SDK.consoleModel.evaluateCommandInConsole( SDK.consoleModel.evaluateCommandInConsole(
executionContext, message, text, /* useCommandLineAPI */ true, /* awaitPromise */ false); 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