forked from mirrors/gecko-dev
Bug 1658238 - Autocomplete source-mapped variable names r=nchevobbe
Differential Revision: https://phabricator.services.mozilla.com/D87117
This commit is contained in:
parent
a2ccfacac3
commit
54751aa56a
14 changed files with 794 additions and 423 deletions
|
|
@ -76,6 +76,8 @@ devtools/client/inspector/markup/test/events_bundle.js
|
||||||
devtools/client/netmonitor/test/xhr_bundle.js
|
devtools/client/netmonitor/test/xhr_bundle.js
|
||||||
devtools/client/webconsole/test/browser/code_bundle_nosource.js
|
devtools/client/webconsole/test/browser/code_bundle_nosource.js
|
||||||
devtools/client/webconsole/test/browser/code_bundle_invalidmap.js
|
devtools/client/webconsole/test/browser/code_bundle_invalidmap.js
|
||||||
|
devtools/client/webconsole/test/browser/test-autocomplete-mapped.js
|
||||||
|
devtools/client/webconsole/test/browser/test-autocomplete-mapped.src.js
|
||||||
devtools/server/tests/xpcshell/setBreakpoint*
|
devtools/server/tests/xpcshell/setBreakpoint*
|
||||||
devtools/server/tests/xpcshell/sourcemapped.js
|
devtools/server/tests/xpcshell/sourcemapped.js
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,19 @@ class DebuggerPanel {
|
||||||
return this._actions.getMappedExpression(expression);
|
return this._actions.getMappedExpression(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the source-mapped variables for the current scope.
|
||||||
|
* @returns {{[String]: String} | null} A dictionary mapping original variable names to generated
|
||||||
|
* variable names if map scopes is enabled, otherwise null.
|
||||||
|
*/
|
||||||
|
getMappedVariables() {
|
||||||
|
if (!this._selectors.isMapScopesEnabled(this._getState())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const thread = this._selectors.getCurrentThread(this._getState());
|
||||||
|
return this._selectors.getSelectedScopeMappings(this._getState(), thread);
|
||||||
|
}
|
||||||
|
|
||||||
isPaused() {
|
isPaused() {
|
||||||
const thread = this._selectors.getCurrentThread(this._getState());
|
const thread = this._selectors.getCurrentThread(this._getState());
|
||||||
return this._selectors.getIsPaused(this._getState(), thread);
|
return this._selectors.getIsPaused(this._getState(), thread);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ const {
|
||||||
AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
|
AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
|
||||||
} = require("devtools/client/webconsole/constants");
|
} = require("devtools/client/webconsole/constants");
|
||||||
|
|
||||||
|
const {
|
||||||
|
analyzeInputString,
|
||||||
|
shouldInputBeAutocompleted,
|
||||||
|
} = require("devtools/shared/webconsole/analyze-input-string");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the data used for the autocomplete popup in the console input (JsTerm).
|
* Update the data used for the autocomplete popup in the console input (JsTerm).
|
||||||
*
|
*
|
||||||
|
|
@ -27,6 +32,8 @@ function autocompleteUpdate(force, getterPath, expressionVars) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputValue = hud.getInputValue();
|
const inputValue = hud.getInputValue();
|
||||||
|
const mappedVars = hud.getMappedVariables() ?? {};
|
||||||
|
const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars));
|
||||||
const frameActorId = await webConsoleUI.getFrameActor();
|
const frameActorId = await webConsoleUI.getFrameActor();
|
||||||
const webconsoleFront = await webConsoleUI.getWebconsoleFront({
|
const webconsoleFront = await webConsoleUI.getWebconsoleFront({
|
||||||
frameActorId,
|
frameActorId,
|
||||||
|
|
@ -43,39 +50,31 @@ function autocompleteUpdate(force, getterPath, expressionVars) {
|
||||||
return dispatch(autocompleteClear());
|
return dispatch(autocompleteClear());
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = inputValue.substring(0, cursor);
|
const rawInput = inputValue.substring(0, cursor);
|
||||||
const retrieveFromCache =
|
const retrieveFromCache =
|
||||||
!force &&
|
!force &&
|
||||||
cache &&
|
cache &&
|
||||||
cache.input &&
|
cache.input &&
|
||||||
input.startsWith(cache.input) &&
|
rawInput.startsWith(cache.input) &&
|
||||||
/[a-zA-Z0-9]$/.test(input) &&
|
/[a-zA-Z0-9]$/.test(rawInput) &&
|
||||||
frameActorId === cache.frameActorId;
|
frameActorId === cache.frameActorId;
|
||||||
|
|
||||||
if (retrieveFromCache) {
|
if (retrieveFromCache) {
|
||||||
return dispatch(autoCompleteDataRetrieveFromCache(input));
|
return dispatch(autoCompleteDataRetrieveFromCache(rawInput));
|
||||||
}
|
}
|
||||||
|
|
||||||
let authorizedEvaluations =
|
const authorizedEvaluations = updateAuthorizedEvaluations(
|
||||||
Array.isArray(state.authorizedEvaluations) &&
|
state.authorizedEvaluations,
|
||||||
state.authorizedEvaluations.length > 0
|
getterPath,
|
||||||
? state.authorizedEvaluations
|
mappedVars
|
||||||
: [];
|
);
|
||||||
|
|
||||||
if (Array.isArray(getterPath) && getterPath.length > 0) {
|
const { input, originalExpression } = await getMappedInput(
|
||||||
// We need to check for any previous authorizations. For example, here if getterPath
|
rawInput,
|
||||||
// is ["a", "b", "c", "d"], we want to see if there was any other path that was
|
mappedVars,
|
||||||
// authorized in a previous request. For that, we only add the previous
|
hud,
|
||||||
// authorizations if the last auth is contained in getterPath. (for the example, we
|
webconsoleFront
|
||||||
// would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
|
);
|
||||||
const last = authorizedEvaluations[authorizedEvaluations.length - 1];
|
|
||||||
const concat = !last || last.every((x, index) => x === getterPath[index]);
|
|
||||||
if (concat) {
|
|
||||||
authorizedEvaluations.push(getterPath);
|
|
||||||
} else {
|
|
||||||
authorizedEvaluations = [getterPath];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(
|
return dispatch(
|
||||||
autocompleteDataFetch({
|
autocompleteDataFetch({
|
||||||
|
|
@ -84,12 +83,116 @@ function autocompleteUpdate(force, getterPath, expressionVars) {
|
||||||
webconsoleFront,
|
webconsoleFront,
|
||||||
authorizedEvaluations,
|
authorizedEvaluations,
|
||||||
force,
|
force,
|
||||||
expressionVars,
|
allVars,
|
||||||
|
mappedVars,
|
||||||
|
originalExpression,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine or replace authorizedEvaluations with the newly authorized getter path, if any.
|
||||||
|
* @param {Array<Array<String>>} authorizedEvaluations Existing authorized evaluations (may
|
||||||
|
* be updated in place)
|
||||||
|
* @param {Array<String>} getterPath The new getter path
|
||||||
|
* @param {{[String]: String}} mappedVars Map of original to generated variable names.
|
||||||
|
* @returns {Array<Array<String>>} The updated authorized evaluations (the original array,
|
||||||
|
* if it was updated in place) */
|
||||||
|
function updateAuthorizedEvaluations(
|
||||||
|
authorizedEvaluations,
|
||||||
|
getterPath,
|
||||||
|
mappedVars
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!Array.isArray(authorizedEvaluations) ||
|
||||||
|
authorizedEvaluations.length == 0
|
||||||
|
) {
|
||||||
|
authorizedEvaluations = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(getterPath) && getterPath.length > 0) {
|
||||||
|
// We need to check for any previous authorizations. For example, here if getterPath
|
||||||
|
// is ["a", "b", "c", "d"], we want to see if there was any other path that was
|
||||||
|
// authorized in a previous request. For that, we only add the previous
|
||||||
|
// authorizations if the last auth is contained in getterPath. (for the example, we
|
||||||
|
// would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
|
||||||
|
const last = authorizedEvaluations[authorizedEvaluations.length - 1];
|
||||||
|
|
||||||
|
const generatedPath = mappedVars[getterPath[0]]?.split(".");
|
||||||
|
if (generatedPath) {
|
||||||
|
getterPath = generatedPath.concat(getterPath.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMappedVariable =
|
||||||
|
generatedPath && getterPath.length === generatedPath.length;
|
||||||
|
const concat = !last || last.every((x, index) => x === getterPath[index]);
|
||||||
|
if (isMappedVariable) {
|
||||||
|
// If the path consists only of an original variable, add all the prefixes of its
|
||||||
|
// mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This
|
||||||
|
// ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both
|
||||||
|
// unsafe getters.
|
||||||
|
authorizedEvaluations = generatedPath.map((_, i) =>
|
||||||
|
generatedPath.slice(0, i + 1)
|
||||||
|
);
|
||||||
|
} else if (concat) {
|
||||||
|
authorizedEvaluations.push(getterPath);
|
||||||
|
} else {
|
||||||
|
authorizedEvaluations = [getterPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authorizedEvaluations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply source mapping to the autocomplete input.
|
||||||
|
* @param {String} rawInput The input to map.
|
||||||
|
* @param {{[String]: String}} mappedVars Map of original to generated variable names.
|
||||||
|
* @param {WebConsole} hud A reference to the webconsole hud.
|
||||||
|
* @param {WebConsoleFront} webconsoleFront The webconsole front.
|
||||||
|
* @returns {String} The source-mapped expression to autocomplete.
|
||||||
|
*/
|
||||||
|
async function getMappedInput(rawInput, mappedVars, hud, webconsoleFront) {
|
||||||
|
if (!mappedVars || Object.keys(mappedVars).length == 0) {
|
||||||
|
return { input: rawInput, originalExpression: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputAnalysis = analyzeInputString(rawInput, 500);
|
||||||
|
if (!shouldInputBeAutocompleted(inputAnalysis)) {
|
||||||
|
return { input: rawInput, originalExpression: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
mainExpression: originalExpression,
|
||||||
|
isPropertyAccess,
|
||||||
|
isElementAccess,
|
||||||
|
lastStatement,
|
||||||
|
} = inputAnalysis;
|
||||||
|
|
||||||
|
// If we're autocompleting a variable name, pass it through unchanged so that we
|
||||||
|
// show original variable names rather than generated ones.
|
||||||
|
// For example, if we have the mapping `myVariable` => `x`, show variables starting
|
||||||
|
// with myVariable rather than x.
|
||||||
|
if (!isPropertyAccess && !isElementAccess) {
|
||||||
|
return { input: lastStatement, originalExpression };
|
||||||
|
}
|
||||||
|
|
||||||
|
let generated =
|
||||||
|
(await hud.getMappedExpression(originalExpression))?.expression ??
|
||||||
|
originalExpression;
|
||||||
|
// Strip off the semicolon if the expression was converted to a statement
|
||||||
|
const trailingSemicolon = /;\s*$/;
|
||||||
|
if (
|
||||||
|
trailingSemicolon.test(generated) &&
|
||||||
|
!trailingSemicolon.test(originalExpression)
|
||||||
|
) {
|
||||||
|
generated = generated.slice(0, generated.lastIndexOf(";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = lastStatement.slice(originalExpression.length);
|
||||||
|
return { input: generated + suffix, originalExpression };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the autocompletion data should be cleared.
|
* Called when the autocompletion data should be cleared.
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,7 +239,9 @@ function autocompleteDataFetch({
|
||||||
force,
|
force,
|
||||||
webconsoleFront,
|
webconsoleFront,
|
||||||
authorizedEvaluations,
|
authorizedEvaluations,
|
||||||
expressionVars,
|
allVars,
|
||||||
|
mappedVars,
|
||||||
|
originalExpression,
|
||||||
}) {
|
}) {
|
||||||
return async ({ dispatch, webConsoleUI }) => {
|
return async ({ dispatch, webConsoleUI }) => {
|
||||||
const selectedNodeActor = webConsoleUI.getSelectedNodeActorID();
|
const selectedNodeActor = webConsoleUI.getSelectedNodeActorID();
|
||||||
|
|
@ -150,10 +255,17 @@ function autocompleteDataFetch({
|
||||||
frameActorId,
|
frameActorId,
|
||||||
selectedNodeActor,
|
selectedNodeActor,
|
||||||
authorizedEvaluations,
|
authorizedEvaluations,
|
||||||
expressionVars
|
allVars
|
||||||
)
|
)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(
|
if (data.isUnsafeGetter && originalExpression !== undefined) {
|
||||||
|
data.getterPath = unmapGetterPath(
|
||||||
|
data.getterPath,
|
||||||
|
originalExpression,
|
||||||
|
mappedVars
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dispatch(
|
||||||
autocompleteDataReceive({
|
autocompleteDataReceive({
|
||||||
id,
|
id,
|
||||||
input,
|
input,
|
||||||
|
|
@ -161,7 +273,6 @@ function autocompleteDataFetch({
|
||||||
frameActorId,
|
frameActorId,
|
||||||
data,
|
data,
|
||||||
authorizedEvaluations,
|
authorizedEvaluations,
|
||||||
expressionVars,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
@ -172,6 +283,38 @@ function autocompleteDataFetch({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace generated variable names in an unsafe getter path with their original
|
||||||
|
* counterparts.
|
||||||
|
* @param {Array<String>} getterPath Array of properties leading up to and including the
|
||||||
|
* unsafe getter.
|
||||||
|
* @param {String} originalExpression The expression that was evaluated, before mapping.
|
||||||
|
* @param {{[String]: String}} mappedVars Map of original to generated variable names.
|
||||||
|
* @returns {Array<String>} An updated getter path containing original variables.
|
||||||
|
*/
|
||||||
|
function unmapGetterPath(getterPath, originalExpression, mappedVars) {
|
||||||
|
// We know that the original expression is a sequence of property accesses, that only
|
||||||
|
// the first part can be a mapped variable, and that the getter path must start with
|
||||||
|
// its generated path or be a prefix of it.
|
||||||
|
|
||||||
|
// Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`.
|
||||||
|
// Get the first part of the expression ("foo")
|
||||||
|
const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim();
|
||||||
|
const generatedVariable = mappedVars[originalVariable];
|
||||||
|
if (generatedVariable) {
|
||||||
|
// Get number of properties in "a.b.c"
|
||||||
|
const generatedVariableParts = generatedVariable.split(".");
|
||||||
|
// Replace ["a", "b", "c"] with "foo" in the getter path.
|
||||||
|
// Note that this will also work if the getter path ends inside of the mapped
|
||||||
|
// variable, like ["a", "b"].
|
||||||
|
return [
|
||||||
|
originalVariable,
|
||||||
|
...getterPath.slice(generatedVariableParts.length),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return getterPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when we receive the autocompletion data from the server.
|
* Called when we receive the autocompletion data from the server.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ subsuite = devtools
|
||||||
support-files =
|
support-files =
|
||||||
head.js
|
head.js
|
||||||
test-autocomplete-in-stackframe.html
|
test-autocomplete-in-stackframe.html
|
||||||
|
test-autocomplete-mapped.html
|
||||||
|
test-autocomplete-mapped.js
|
||||||
|
test-autocomplete-mapped.js.map
|
||||||
|
test-autocomplete-mapped.src.js
|
||||||
test-block-action.html
|
test-block-action.html
|
||||||
test-block-action-style.css
|
test-block-action-style.css
|
||||||
test-console-evaluation-context-selector-child.html
|
test-console-evaluation-context-selector-child.html
|
||||||
|
|
@ -54,6 +58,7 @@ skip-if = debug && (os == "win" && bits == 32) #bug 1620638
|
||||||
skip-if = (os == "win" && os_version == "6.1") # Bug 1620521
|
skip-if = (os == "win" && os_version == "6.1") # Bug 1620521
|
||||||
[browser_jsterm_autocomplete_inside_text.js]
|
[browser_jsterm_autocomplete_inside_text.js]
|
||||||
skip-if = (os == "win" && os_version == "6.1") # Bug 1620521
|
skip-if = (os == "win" && os_version == "6.1") # Bug 1620521
|
||||||
|
[browser_jsterm_autocomplete_mapped_variables.js]
|
||||||
[browser_jsterm_autocomplete_native_getters.js]
|
[browser_jsterm_autocomplete_native_getters.js]
|
||||||
[browser_jsterm_autocomplete_nav_and_tab_key.js]
|
[browser_jsterm_autocomplete_nav_and_tab_key.js]
|
||||||
[browser_jsterm_autocomplete_null.js]
|
[browser_jsterm_autocomplete_null.js]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
// Test that makes sure source mapped variables appear in autocompletion
|
||||||
|
// on an equal footing with variables from the generated source.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
/* import-globals-from head.js*/
|
||||||
|
|
||||||
|
const TEST_URI =
|
||||||
|
"http://example.com/browser/devtools/client/webconsole/" +
|
||||||
|
"test/browser/test-autocomplete-mapped.html";
|
||||||
|
|
||||||
|
add_task(async function() {
|
||||||
|
const hud = await openNewTabAndConsole(TEST_URI);
|
||||||
|
const { jsterm } = hud;
|
||||||
|
const { autocompletePopup: popup } = jsterm;
|
||||||
|
const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
|
||||||
|
|
||||||
|
info("Opening Debugger and enabling map scopes");
|
||||||
|
await openDebugger();
|
||||||
|
const dbg = createDebuggerContext(toolbox);
|
||||||
|
dbg.actions.toggleMapScopes();
|
||||||
|
|
||||||
|
info("Waiting for pause");
|
||||||
|
// This calls firstCall() on the content page and waits for pause. (firstCall
|
||||||
|
// has a debugger statement)
|
||||||
|
await pauseDebugger(dbg);
|
||||||
|
|
||||||
|
await toolbox.selectTool("webconsole");
|
||||||
|
await setInputValueForAutocompletion(hud, "valu");
|
||||||
|
ok(
|
||||||
|
hasExactPopupLabels(popup, ["value", "valueOf", "values"]),
|
||||||
|
"Autocomplete popup displays original variable name"
|
||||||
|
);
|
||||||
|
|
||||||
|
await setInputValueForAutocompletion(hud, "temp");
|
||||||
|
ok(
|
||||||
|
hasExactPopupLabels(popup, ["temp", "temp2"]),
|
||||||
|
"Autocomplete popup displays original variable name when entering a complete variable name"
|
||||||
|
);
|
||||||
|
|
||||||
|
await setInputValueForAutocompletion(hud, "t");
|
||||||
|
ok(
|
||||||
|
hasPopupLabel(popup, "t"),
|
||||||
|
"Autocomplete popup displays generated variable name"
|
||||||
|
);
|
||||||
|
|
||||||
|
await setInputValueForAutocompletion(hud, "value.to");
|
||||||
|
ok(
|
||||||
|
hasPopupLabel(popup, "toString"),
|
||||||
|
"Autocomplete popup displays properties of original variable"
|
||||||
|
);
|
||||||
|
|
||||||
|
await setInputValueForAutocompletion(hud, "imported.imp");
|
||||||
|
ok(
|
||||||
|
hasPopupLabel(popup, "importResult"),
|
||||||
|
"Autocomplete popup displays properties of multi-part variable"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tooltip = await setInputValueForGetterConfirmDialog(
|
||||||
|
toolbox,
|
||||||
|
hud,
|
||||||
|
"getter."
|
||||||
|
);
|
||||||
|
let labelEl = tooltip.querySelector(".confirm-label");
|
||||||
|
is(
|
||||||
|
labelEl.textContent,
|
||||||
|
"Invoke getter getter to retrieve the property list?",
|
||||||
|
"Dialog has expected text content"
|
||||||
|
);
|
||||||
|
|
||||||
|
info(
|
||||||
|
"Check that getter confirmation on a variable that maps to two getters invokes both getters"
|
||||||
|
);
|
||||||
|
let onPopUpOpen = popup.once("popup-opened");
|
||||||
|
EventUtils.synthesizeKey("KEY_Tab");
|
||||||
|
await onPopUpOpen;
|
||||||
|
ok(popup.isOpen, "popup is open after Tab");
|
||||||
|
ok(hasPopupLabel(popup, "getterResult"), "popup has expected items");
|
||||||
|
|
||||||
|
info(
|
||||||
|
"Check that the getter confirmation dialog shows the original variable name"
|
||||||
|
);
|
||||||
|
tooltip = await setInputValueForGetterConfirmDialog(
|
||||||
|
toolbox,
|
||||||
|
hud,
|
||||||
|
"localWithGetter.value."
|
||||||
|
);
|
||||||
|
labelEl = tooltip.querySelector(".confirm-label");
|
||||||
|
is(
|
||||||
|
labelEl.textContent,
|
||||||
|
"Invoke getter localWithGetter.value to retrieve the property list?",
|
||||||
|
"Dialog has expected text content"
|
||||||
|
);
|
||||||
|
|
||||||
|
info(
|
||||||
|
"Check that hitting Tab does invoke the getter and return its properties"
|
||||||
|
);
|
||||||
|
onPopUpOpen = popup.once("popup-opened");
|
||||||
|
EventUtils.synthesizeKey("KEY_Tab");
|
||||||
|
await onPopUpOpen;
|
||||||
|
ok(popup.isOpen, "popup is open after Tab");
|
||||||
|
ok(hasPopupLabel(popup, "then"), "popup has expected items");
|
||||||
|
info("got popup items: " + JSON.stringify(getAutocompletePopupLabels(popup)));
|
||||||
|
|
||||||
|
info(
|
||||||
|
"Check that authorizing an original getter applies to the generated getter"
|
||||||
|
);
|
||||||
|
await setInputValueForAutocompletion(hud, "o.value.");
|
||||||
|
ok(hasPopupLabel(popup, "then"), "popup has expected items");
|
||||||
|
|
||||||
|
await setInputValueForAutocompletion(hud, "(temp + temp2).");
|
||||||
|
ok(
|
||||||
|
hasPopupLabel(popup, "toFixed"),
|
||||||
|
"Autocomplete popup displays properties of eagerly evaluated value"
|
||||||
|
);
|
||||||
|
info("got popup items: " + JSON.stringify(getAutocompletePopupLabels(popup)));
|
||||||
|
|
||||||
|
info("Disabling map scopes");
|
||||||
|
dbg.actions.toggleMapScopes();
|
||||||
|
await setInputValueForAutocompletion(hud, "tem");
|
||||||
|
const autocompleteLabels = getAutocompletePopupLabels(popup);
|
||||||
|
ok(
|
||||||
|
!autocompleteLabels.includes("temp"),
|
||||||
|
"Autocomplete popup does not display mapped variables when mapping is disabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
await resume(dbg);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html dir="ltr" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<!--
|
||||||
|
- Any copyright is dedicated to the Public Domain.
|
||||||
|
- http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
-->
|
||||||
|
<title>Test for autocomplete displaying mapped variable names</title>
|
||||||
|
<script src="test-autocomplete-mapped.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Hello world!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
"use strict";
|
||||||
|
const i = { x: { y: { importResult: true } } };
|
||||||
|
const j = { get x() { return blackbox({ get y() { return blackbox({ getterResult: 1 }); } }); } };
|
||||||
|
|
||||||
|
const blackbox = x=>[x].pop();
|
||||||
|
|
||||||
|
function firstCall() {
|
||||||
|
const t = 42;
|
||||||
|
const u = i.x.y;
|
||||||
|
const v = j.x.y.getterResult;
|
||||||
|
const o = {
|
||||||
|
get value() {
|
||||||
|
return blackbox(Promise.resolve());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=test-autocomplete-mapped.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"sources":["test-autocomplete-mapped.src.js"],"names":["blackbox","x","pop","firstCall","value","imported","getter","localWithGetter","Promise","resolve"],"mappings":"AAAA;AACA,MAAS,CAAQ;AACjB,MAAS,CAAM;;AAEf,MAAMA,WAAWC,GAAK,CAACA,GAAGC;;AAE1B,SAASC;EACP,MAAMC,IAAQ;EACd,MAAMC,IAAO,KAAQ;EACrB,MAAMC,IAAQ,kBAAM;EACpB,MAAMC,IAAkB;IACtBH;MAAc,OAAOJ,SAASQ,QAAQC;;;EAGxC","file":"test-autocomplete-mapped.js"}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"use strict";
|
||||||
|
import { imported } from "somewhere";
|
||||||
|
import { getter } from "somewhere-else";
|
||||||
|
|
||||||
|
const blackbox = x => [x].pop();
|
||||||
|
|
||||||
|
function firstCall() {
|
||||||
|
const value = 42;
|
||||||
|
const temp = imported;
|
||||||
|
const temp2 = getter;
|
||||||
|
const localWithGetter = {
|
||||||
|
get value() { return blackbox(Promise.resolve()); }
|
||||||
|
};
|
||||||
|
const unmapped = 100;
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
|
@ -322,6 +322,11 @@ class WebConsole {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMappedVariables() {
|
||||||
|
const { toolbox } = this;
|
||||||
|
return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
|
||||||
|
}
|
||||||
|
|
||||||
get parserService() {
|
get parserService() {
|
||||||
if (this._parserService) {
|
if (this._parserService) {
|
||||||
return this._parserService;
|
return this._parserService;
|
||||||
|
|
|
||||||
406
devtools/shared/webconsole/analyze-input-string.js
Normal file
406
devtools/shared/webconsole/analyze-input-string.js
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const STATE_NORMAL = Symbol("STATE_NORMAL");
|
||||||
|
const STATE_QUOTE = Symbol("STATE_QUOTE");
|
||||||
|
const STATE_DQUOTE = Symbol("STATE_DQUOTE");
|
||||||
|
const STATE_TEMPLATE_LITERAL = Symbol("STATE_TEMPLATE_LITERAL");
|
||||||
|
const STATE_ESCAPE_QUOTE = Symbol("STATE_ESCAPE_QUOTE");
|
||||||
|
const STATE_ESCAPE_DQUOTE = Symbol("STATE_ESCAPE_DQUOTE");
|
||||||
|
const STATE_ESCAPE_TEMPLATE_LITERAL = Symbol("STATE_ESCAPE_TEMPLATE_LITERAL");
|
||||||
|
const STATE_SLASH = Symbol("STATE_SLASH");
|
||||||
|
const STATE_INLINE_COMMENT = Symbol("STATE_INLINE_COMMENT");
|
||||||
|
const STATE_MULTILINE_COMMENT = Symbol("STATE_MULTILINE_COMMENT");
|
||||||
|
const STATE_MULTILINE_COMMENT_CLOSE = Symbol("STATE_MULTILINE_COMMENT_CLOSE");
|
||||||
|
const STATE_QUESTION_MARK = Symbol("STATE_QUESTION_MARK");
|
||||||
|
|
||||||
|
const OPEN_BODY = "{[(".split("");
|
||||||
|
const CLOSE_BODY = "}])".split("");
|
||||||
|
const OPEN_CLOSE_BODY = {
|
||||||
|
"{": "}",
|
||||||
|
"[": "]",
|
||||||
|
"(": ")",
|
||||||
|
};
|
||||||
|
|
||||||
|
const NO_AUTOCOMPLETE_PREFIXES = ["var", "const", "let", "function", "class"];
|
||||||
|
const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~!".split(""));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyses a given string to find the last statement that is interesting for
|
||||||
|
* later completion.
|
||||||
|
*
|
||||||
|
* @param string str
|
||||||
|
* A string to analyse.
|
||||||
|
*
|
||||||
|
* @returns object
|
||||||
|
* If there was an error in the string detected, then a object like
|
||||||
|
*
|
||||||
|
* { err: "ErrorMesssage" }
|
||||||
|
*
|
||||||
|
* is returned, otherwise a object like
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
|
||||||
|
* lastStatement: the last statement in the string,
|
||||||
|
* isElementAccess: boolean that indicates if the lastStatement has an open
|
||||||
|
* element access (e.g. `x["match`).
|
||||||
|
* isPropertyAccess: boolean indicating if we are accessing property
|
||||||
|
* (e.g `true` in `var a = {b: 1};a.b`)
|
||||||
|
* matchProp: The part of the expression that should match the properties
|
||||||
|
* on the mainExpression (e.g. `que` when expression is `document.body.que`)
|
||||||
|
* mainExpression: The part of the expression before any property access,
|
||||||
|
* (e.g. `a.b` if expression is `a.b.`)
|
||||||
|
* expressionBeforePropertyAccess: The part of the expression before property access
|
||||||
|
* (e.g `var a = {b: 1};a` if expression is `var a = {b: 1};a.b`)
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
exports.analyzeInputString = function(str, timeout = 2500) {
|
||||||
|
// work variables.
|
||||||
|
const bodyStack = [];
|
||||||
|
let state = STATE_NORMAL;
|
||||||
|
let previousNonWhitespaceChar;
|
||||||
|
let lastStatement = "";
|
||||||
|
let currentIndex = -1;
|
||||||
|
let dotIndex;
|
||||||
|
let pendingWhitespaceChars = "";
|
||||||
|
const startingTime = Date.now();
|
||||||
|
|
||||||
|
// Use a string iterator in order to handle character with a length >= 2 (e.g. 😎).
|
||||||
|
for (const c of str) {
|
||||||
|
// We are possibly dealing with a very large string that would take a long time to
|
||||||
|
// analyze (and freeze the process). If the function has been running for more than
|
||||||
|
// a given time, we stop the analysis (this isn't too bad because the only
|
||||||
|
// consequence is that we won't provide autocompletion items).
|
||||||
|
if (Date.now() - startingTime > timeout) {
|
||||||
|
return {
|
||||||
|
err: "timeout",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex += 1;
|
||||||
|
let resetLastStatement = false;
|
||||||
|
const isWhitespaceChar = c.trim() === "";
|
||||||
|
switch (state) {
|
||||||
|
// Normal JS state.
|
||||||
|
case STATE_NORMAL:
|
||||||
|
if (lastStatement.endsWith("?.") && /\d/.test(c)) {
|
||||||
|
// If the current char is a number, the engine will consider we're not
|
||||||
|
// performing an optional chaining, but a ternary (e.g. x ?.4 : 2).
|
||||||
|
lastStatement = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storing the index of dot of the input string
|
||||||
|
if (c === ".") {
|
||||||
|
dotIndex = currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last characters were spaces, and the current one is not.
|
||||||
|
if (pendingWhitespaceChars && !isWhitespaceChar) {
|
||||||
|
// If we have a legitimate property/element access, or potential optional
|
||||||
|
// chaining call, we append the spaces.
|
||||||
|
if (c === "[" || c === "." || c === "?") {
|
||||||
|
lastStatement = lastStatement + pendingWhitespaceChars;
|
||||||
|
} else {
|
||||||
|
// if not, we can be sure the statement was over, and we can start a new one.
|
||||||
|
lastStatement = "";
|
||||||
|
}
|
||||||
|
pendingWhitespaceChars = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"') {
|
||||||
|
state = STATE_DQUOTE;
|
||||||
|
} else if (c == "'") {
|
||||||
|
state = STATE_QUOTE;
|
||||||
|
} else if (c == "`") {
|
||||||
|
state = STATE_TEMPLATE_LITERAL;
|
||||||
|
} else if (c == "/") {
|
||||||
|
state = STATE_SLASH;
|
||||||
|
} else if (c == "?") {
|
||||||
|
state = STATE_QUESTION_MARK;
|
||||||
|
} else if (OPERATOR_CHARS_SET.has(c)) {
|
||||||
|
// If the character is an operator, we can update the current statement.
|
||||||
|
resetLastStatement = true;
|
||||||
|
} else if (isWhitespaceChar) {
|
||||||
|
// If the previous char isn't a dot or opening bracket, and the current computed
|
||||||
|
// statement is not a variable/function/class declaration, we track the number
|
||||||
|
// of consecutive spaces, so we can re-use them at some point (or drop them).
|
||||||
|
if (
|
||||||
|
previousNonWhitespaceChar !== "." &&
|
||||||
|
previousNonWhitespaceChar !== "[" &&
|
||||||
|
!NO_AUTOCOMPLETE_PREFIXES.includes(lastStatement)
|
||||||
|
) {
|
||||||
|
pendingWhitespaceChars += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (OPEN_BODY.includes(c)) {
|
||||||
|
// When opening a bracket or a parens, we store the current statement, in order
|
||||||
|
// to be able to retrieve it later.
|
||||||
|
bodyStack.push({
|
||||||
|
token: c,
|
||||||
|
lastStatement,
|
||||||
|
index: currentIndex,
|
||||||
|
});
|
||||||
|
// And we compute a new statement.
|
||||||
|
resetLastStatement = true;
|
||||||
|
} else if (CLOSE_BODY.includes(c)) {
|
||||||
|
const last = bodyStack.pop();
|
||||||
|
if (!last || OPEN_CLOSE_BODY[last.token] != c) {
|
||||||
|
return {
|
||||||
|
err: "syntax error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (c == "}") {
|
||||||
|
resetLastStatement = true;
|
||||||
|
} else {
|
||||||
|
lastStatement = last.lastStatement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Escaped quote
|
||||||
|
case STATE_ESCAPE_QUOTE:
|
||||||
|
state = STATE_QUOTE;
|
||||||
|
break;
|
||||||
|
case STATE_ESCAPE_DQUOTE:
|
||||||
|
state = STATE_DQUOTE;
|
||||||
|
break;
|
||||||
|
case STATE_ESCAPE_TEMPLATE_LITERAL:
|
||||||
|
state = STATE_TEMPLATE_LITERAL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Double quote state > " <
|
||||||
|
case STATE_DQUOTE:
|
||||||
|
if (c == "\\") {
|
||||||
|
state = STATE_ESCAPE_DQUOTE;
|
||||||
|
} else if (c == "\n") {
|
||||||
|
return {
|
||||||
|
err: "unterminated string literal",
|
||||||
|
};
|
||||||
|
} else if (c == '"') {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Template literal state > ` <
|
||||||
|
case STATE_TEMPLATE_LITERAL:
|
||||||
|
if (c == "\\") {
|
||||||
|
state = STATE_ESCAPE_TEMPLATE_LITERAL;
|
||||||
|
} else if (c == "`") {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Single quote state > ' <
|
||||||
|
case STATE_QUOTE:
|
||||||
|
if (c == "\\") {
|
||||||
|
state = STATE_ESCAPE_QUOTE;
|
||||||
|
} else if (c == "\n") {
|
||||||
|
return {
|
||||||
|
err: "unterminated string literal",
|
||||||
|
};
|
||||||
|
} else if (c == "'") {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case STATE_SLASH:
|
||||||
|
if (c == "/") {
|
||||||
|
state = STATE_INLINE_COMMENT;
|
||||||
|
} else if (c == "*") {
|
||||||
|
state = STATE_MULTILINE_COMMENT;
|
||||||
|
} else {
|
||||||
|
lastStatement = "";
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_INLINE_COMMENT:
|
||||||
|
if (c === "\n") {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
resetLastStatement = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_MULTILINE_COMMENT:
|
||||||
|
if (c === "*") {
|
||||||
|
state = STATE_MULTILINE_COMMENT_CLOSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_MULTILINE_COMMENT_CLOSE:
|
||||||
|
if (c === "/") {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
resetLastStatement = true;
|
||||||
|
} else {
|
||||||
|
state = STATE_MULTILINE_COMMENT;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_QUESTION_MARK:
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
if (c === "?") {
|
||||||
|
// If we have a nullish coalescing operator, we start a new statement
|
||||||
|
resetLastStatement = true;
|
||||||
|
} else if (c !== ".") {
|
||||||
|
// If we're not dealing with optional chaining (?.), it means we have a ternary,
|
||||||
|
// so we are starting a new statement that includes the current character.
|
||||||
|
lastStatement = "";
|
||||||
|
} else {
|
||||||
|
dotIndex = currentIndex;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWhitespaceChar) {
|
||||||
|
previousNonWhitespaceChar = c;
|
||||||
|
}
|
||||||
|
if (resetLastStatement) {
|
||||||
|
lastStatement = "";
|
||||||
|
} else {
|
||||||
|
lastStatement = lastStatement + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We update all the open stacks lastStatement so they are up-to-date.
|
||||||
|
bodyStack.forEach(stack => {
|
||||||
|
if (stack.token !== "}") {
|
||||||
|
stack.lastStatement = stack.lastStatement + c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let isElementAccess = false;
|
||||||
|
let lastOpeningBracketIndex = -1;
|
||||||
|
if (bodyStack.length === 1 && bodyStack[0].token === "[") {
|
||||||
|
lastStatement = bodyStack[0].lastStatement;
|
||||||
|
lastOpeningBracketIndex = bodyStack[0].index;
|
||||||
|
isElementAccess = true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
state === STATE_DQUOTE ||
|
||||||
|
state === STATE_QUOTE ||
|
||||||
|
state === STATE_TEMPLATE_LITERAL ||
|
||||||
|
state === STATE_ESCAPE_QUOTE ||
|
||||||
|
state === STATE_ESCAPE_DQUOTE ||
|
||||||
|
state === STATE_ESCAPE_TEMPLATE_LITERAL
|
||||||
|
) {
|
||||||
|
state = STATE_NORMAL;
|
||||||
|
}
|
||||||
|
} else if (pendingWhitespaceChars) {
|
||||||
|
lastStatement = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCompletionCharIndex = isElementAccess
|
||||||
|
? lastOpeningBracketIndex
|
||||||
|
: dotIndex;
|
||||||
|
|
||||||
|
const stringBeforeLastCompletionChar = str.slice(0, lastCompletionCharIndex);
|
||||||
|
|
||||||
|
const isPropertyAccess =
|
||||||
|
lastCompletionCharIndex && lastCompletionCharIndex > 0;
|
||||||
|
|
||||||
|
// Compute `isOptionalAccess`, so that we can use it
|
||||||
|
// later for computing `expressionBeforePropertyAccess`.
|
||||||
|
//Check `?.` before `[` for element access ( e.g `a?.["b` or `a ?. ["b` )
|
||||||
|
// and `?` before `.` for regular property access ( e.g `a?.b` or `a ?. b` )
|
||||||
|
const optionalElementAccessRegex = /\?\.\s*$/;
|
||||||
|
const isOptionalAccess = isElementAccess
|
||||||
|
? optionalElementAccessRegex.test(stringBeforeLastCompletionChar)
|
||||||
|
: isPropertyAccess &&
|
||||||
|
str.slice(lastCompletionCharIndex - 1, lastCompletionCharIndex + 1) ===
|
||||||
|
"?.";
|
||||||
|
|
||||||
|
// Get the filtered string for the properties (e.g if `document.qu` then `qu`)
|
||||||
|
const matchProp = isPropertyAccess
|
||||||
|
? str.slice(lastCompletionCharIndex + 1).trimLeft()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const expressionBeforePropertyAccess = isPropertyAccess
|
||||||
|
? str.slice(
|
||||||
|
0,
|
||||||
|
// For optional access, we can take all the chars before the last "?" char.
|
||||||
|
isOptionalAccess
|
||||||
|
? stringBeforeLastCompletionChar.lastIndexOf("?")
|
||||||
|
: lastCompletionCharIndex
|
||||||
|
)
|
||||||
|
: str;
|
||||||
|
|
||||||
|
let mainExpression = lastStatement;
|
||||||
|
if (isPropertyAccess) {
|
||||||
|
if (isOptionalAccess) {
|
||||||
|
// Strip anything before the last `?`.
|
||||||
|
mainExpression = mainExpression.slice(0, mainExpression.lastIndexOf("?"));
|
||||||
|
} else {
|
||||||
|
mainExpression = mainExpression.slice(
|
||||||
|
0,
|
||||||
|
-1 * (str.length - lastCompletionCharIndex)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainExpression = mainExpression.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
isElementAccess,
|
||||||
|
isPropertyAccess,
|
||||||
|
expressionBeforePropertyAccess,
|
||||||
|
lastStatement,
|
||||||
|
mainExpression,
|
||||||
|
matchProp,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the analyzed input string is in an appropriate state to autocomplete, e.g. not
|
||||||
|
* inside a string, or declaring a variable.
|
||||||
|
* @param {object} inputAnalysisState The analyzed string to check
|
||||||
|
* @returns {boolean} Whether the input should be autocompleted
|
||||||
|
*/
|
||||||
|
exports.shouldInputBeAutocompleted = function(inputAnalysisState) {
|
||||||
|
const { err, state, lastStatement } = inputAnalysisState;
|
||||||
|
|
||||||
|
// There was an error analysing the string.
|
||||||
|
if (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current state is not STATE_NORMAL, then we are inside string,
|
||||||
|
// which means that no completion is possible.
|
||||||
|
if (state != STATE_NORMAL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't complete on just an empty string.
|
||||||
|
if (lastStatement.trim() == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
NO_AUTOCOMPLETE_PREFIXES.some(prefix =>
|
||||||
|
lastStatement.startsWith(prefix + " ")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the analyzed input string is in an appropriate state to be eagerly evaluated.
|
||||||
|
* @param {object} inputAnalysisState
|
||||||
|
* @returns {boolean} Whether the input should be eagerly evaluated
|
||||||
|
*/
|
||||||
|
exports.shouldInputBeEagerlyEvaluated = function({ lastStatement }) {
|
||||||
|
const inComputedProperty =
|
||||||
|
lastStatement.lastIndexOf("[") !== -1 &&
|
||||||
|
lastStatement.lastIndexOf("[") > lastStatement.lastIndexOf("]");
|
||||||
|
|
||||||
|
const hasPropertyAccess =
|
||||||
|
lastStatement.includes(".") || lastStatement.includes("[");
|
||||||
|
|
||||||
|
return hasPropertyAccess && !inComputedProperty;
|
||||||
|
};
|
||||||
|
|
@ -24,6 +24,16 @@ loader.lazyRequireGetter(
|
||||||
"resource://gre/modules/reflect.jsm",
|
"resource://gre/modules/reflect.jsm",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
loader.lazyRequireGetter(
|
||||||
|
this,
|
||||||
|
[
|
||||||
|
"analyzeInputString",
|
||||||
|
"shouldInputBeAutocompleted",
|
||||||
|
"shouldInputBeEagerlyEvaluated",
|
||||||
|
],
|
||||||
|
"devtools/shared/webconsole/analyze-input-string",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Provide an easy way to bail out of even attempting an autocompletion
|
// Provide an easy way to bail out of even attempting an autocompletion
|
||||||
// if an object has way too many properties. Protects against large objects
|
// if an object has way too many properties. Protects against large objects
|
||||||
|
|
@ -355,401 +365,10 @@ function JSPropertyProvider({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldInputBeEagerlyEvaluated({ lastStatement }) {
|
|
||||||
const inComputedProperty =
|
|
||||||
lastStatement.lastIndexOf("[") !== -1 &&
|
|
||||||
lastStatement.lastIndexOf("[") > lastStatement.lastIndexOf("]");
|
|
||||||
|
|
||||||
const hasPropertyAccess =
|
|
||||||
lastStatement.includes(".") || lastStatement.includes("[");
|
|
||||||
|
|
||||||
return hasPropertyAccess && !inComputedProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldInputBeAutocompleted(inputAnalysisState) {
|
|
||||||
const { err, state, lastStatement } = inputAnalysisState;
|
|
||||||
|
|
||||||
// There was an error analysing the string.
|
|
||||||
if (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current state is not STATE_NORMAL, then we are inside of an string
|
|
||||||
// which means that no completion is possible.
|
|
||||||
if (state != STATE_NORMAL) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't complete on just an empty string.
|
|
||||||
if (lastStatement.trim() == "") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
NO_AUTOCOMPLETE_PREFIXES.some(prefix =>
|
|
||||||
lastStatement.startsWith(prefix + " ")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasArrayIndex(str) {
|
function hasArrayIndex(str) {
|
||||||
return /\[\d+\]$/.test(str);
|
return /\[\d+\]$/.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_NORMAL = Symbol("STATE_NORMAL");
|
|
||||||
const STATE_QUOTE = Symbol("STATE_QUOTE");
|
|
||||||
const STATE_DQUOTE = Symbol("STATE_DQUOTE");
|
|
||||||
const STATE_TEMPLATE_LITERAL = Symbol("STATE_TEMPLATE_LITERAL");
|
|
||||||
const STATE_ESCAPE_QUOTE = Symbol("STATE_ESCAPE_QUOTE");
|
|
||||||
const STATE_ESCAPE_DQUOTE = Symbol("STATE_ESCAPE_DQUOTE");
|
|
||||||
const STATE_ESCAPE_TEMPLATE_LITERAL = Symbol("STATE_ESCAPE_TEMPLATE_LITERAL");
|
|
||||||
const STATE_SLASH = Symbol("STATE_SLASH");
|
|
||||||
const STATE_INLINE_COMMENT = Symbol("STATE_INLINE_COMMENT");
|
|
||||||
const STATE_MULTILINE_COMMENT = Symbol("STATE_MULTILINE_COMMENT");
|
|
||||||
const STATE_MULTILINE_COMMENT_CLOSE = Symbol("STATE_MULTILINE_COMMENT_CLOSE");
|
|
||||||
const STATE_QUESTION_MARK = Symbol("STATE_QUESTION_MARK");
|
|
||||||
|
|
||||||
const OPEN_BODY = "{[(".split("");
|
|
||||||
const CLOSE_BODY = "}])".split("");
|
|
||||||
const OPEN_CLOSE_BODY = {
|
|
||||||
"{": "}",
|
|
||||||
"[": "]",
|
|
||||||
"(": ")",
|
|
||||||
};
|
|
||||||
|
|
||||||
const NO_AUTOCOMPLETE_PREFIXES = ["var", "const", "let", "function", "class"];
|
|
||||||
const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~!".split(""));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyses a given string to find the last statement that is interesting for
|
|
||||||
* later completion.
|
|
||||||
*
|
|
||||||
* @param string str
|
|
||||||
* A string to analyse.
|
|
||||||
*
|
|
||||||
* @returns object
|
|
||||||
* If there was an error in the string detected, then a object like
|
|
||||||
*
|
|
||||||
* { err: "ErrorMesssage" }
|
|
||||||
*
|
|
||||||
* is returned, otherwise a object like
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
|
|
||||||
* lastStatement: the last statement in the string,
|
|
||||||
* isElementAccess: boolean that indicates if the lastStatement has an open
|
|
||||||
* element access (e.g. `x["match`).
|
|
||||||
* isPropertyAccess: boolean indicating if we are accessing property
|
|
||||||
* (e.g `true` in `var a = {b: 1};a.b`)
|
|
||||||
* matchProp: The part of the expression that should match the properties
|
|
||||||
* on the mainExpression (e.g. `que` when expression is `document.body.que`)
|
|
||||||
* mainExpression: The part of the expression before any property access,
|
|
||||||
* (e.g. `a.b` if expression is `a.b.`)
|
|
||||||
* expressionBeforePropertyAccess: The part of the expression before property access
|
|
||||||
* (e.g `var a = {b: 1};a` if expression is `var a = {b: 1};a.b`)
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line complexity
|
|
||||||
function analyzeInputString(str) {
|
|
||||||
// work variables.
|
|
||||||
const bodyStack = [];
|
|
||||||
let state = STATE_NORMAL;
|
|
||||||
let previousNonWhitespaceChar;
|
|
||||||
let lastStatement = "";
|
|
||||||
let currentIndex = -1;
|
|
||||||
let dotIndex;
|
|
||||||
let pendingWhitespaceChars = "";
|
|
||||||
const TIMEOUT = 2500;
|
|
||||||
const startingTime = Date.now();
|
|
||||||
|
|
||||||
// Use a string iterator in order to handle character with a length >= 2 (e.g. 😎).
|
|
||||||
for (const c of str) {
|
|
||||||
// We are possibly dealing with a very large string that would take a long time to
|
|
||||||
// analyze (and freeze the process). If the function has been running for more than
|
|
||||||
// a given time, we stop the analysis (this isn't too bad because the only
|
|
||||||
// consequence is that we won't provide autocompletion items).
|
|
||||||
if (Date.now() - startingTime > TIMEOUT) {
|
|
||||||
return {
|
|
||||||
err: "timeout",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex += 1;
|
|
||||||
let resetLastStatement = false;
|
|
||||||
const isWhitespaceChar = c.trim() === "";
|
|
||||||
switch (state) {
|
|
||||||
// Normal JS state.
|
|
||||||
case STATE_NORMAL:
|
|
||||||
if (lastStatement.endsWith("?.") && /\d/.test(c)) {
|
|
||||||
// If the current char is a number, the engine will consider we're not
|
|
||||||
// performing an optional chaining, but a ternary (e.g. x ?.4 : 2).
|
|
||||||
lastStatement = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storing the index of dot of the input string
|
|
||||||
if (c === ".") {
|
|
||||||
dotIndex = currentIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last characters were spaces, and the current one is not.
|
|
||||||
if (pendingWhitespaceChars && !isWhitespaceChar) {
|
|
||||||
// If we have a legitimate property/element access, or potential optional
|
|
||||||
// chaining call, we append the spaces.
|
|
||||||
if (c === "[" || c === "." || c === "?") {
|
|
||||||
lastStatement = lastStatement + pendingWhitespaceChars;
|
|
||||||
} else {
|
|
||||||
// if not, we can be sure the statement was over, and we can start a new one.
|
|
||||||
lastStatement = "";
|
|
||||||
}
|
|
||||||
pendingWhitespaceChars = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == '"') {
|
|
||||||
state = STATE_DQUOTE;
|
|
||||||
} else if (c == "'") {
|
|
||||||
state = STATE_QUOTE;
|
|
||||||
} else if (c == "`") {
|
|
||||||
state = STATE_TEMPLATE_LITERAL;
|
|
||||||
} else if (c == "/") {
|
|
||||||
state = STATE_SLASH;
|
|
||||||
} else if (c == "?") {
|
|
||||||
state = STATE_QUESTION_MARK;
|
|
||||||
} else if (OPERATOR_CHARS_SET.has(c)) {
|
|
||||||
// If the character is an operator, we can update the current statement.
|
|
||||||
resetLastStatement = true;
|
|
||||||
} else if (isWhitespaceChar) {
|
|
||||||
// If the previous char isn't a dot or opening bracket, and the current computed
|
|
||||||
// statement is not a variable/function/class declaration, we track the number
|
|
||||||
// of consecutive spaces, so we can re-use them at some point (or drop them).
|
|
||||||
if (
|
|
||||||
previousNonWhitespaceChar !== "." &&
|
|
||||||
previousNonWhitespaceChar !== "[" &&
|
|
||||||
!NO_AUTOCOMPLETE_PREFIXES.includes(lastStatement)
|
|
||||||
) {
|
|
||||||
pendingWhitespaceChars += c;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if (OPEN_BODY.includes(c)) {
|
|
||||||
// When opening a bracket or a parens, we store the current statement, in order
|
|
||||||
// to be able to retrieve it later.
|
|
||||||
bodyStack.push({
|
|
||||||
token: c,
|
|
||||||
lastStatement,
|
|
||||||
index: currentIndex,
|
|
||||||
});
|
|
||||||
// And we compute a new statement.
|
|
||||||
resetLastStatement = true;
|
|
||||||
} else if (CLOSE_BODY.includes(c)) {
|
|
||||||
const last = bodyStack.pop();
|
|
||||||
if (!last || OPEN_CLOSE_BODY[last.token] != c) {
|
|
||||||
return {
|
|
||||||
err: "syntax error",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (c == "}") {
|
|
||||||
resetLastStatement = true;
|
|
||||||
} else {
|
|
||||||
lastStatement = last.lastStatement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Escaped quote
|
|
||||||
case STATE_ESCAPE_QUOTE:
|
|
||||||
state = STATE_QUOTE;
|
|
||||||
break;
|
|
||||||
case STATE_ESCAPE_DQUOTE:
|
|
||||||
state = STATE_DQUOTE;
|
|
||||||
break;
|
|
||||||
case STATE_ESCAPE_TEMPLATE_LITERAL:
|
|
||||||
state = STATE_TEMPLATE_LITERAL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Double quote state > " <
|
|
||||||
case STATE_DQUOTE:
|
|
||||||
if (c == "\\") {
|
|
||||||
state = STATE_ESCAPE_DQUOTE;
|
|
||||||
} else if (c == "\n") {
|
|
||||||
return {
|
|
||||||
err: "unterminated string literal",
|
|
||||||
};
|
|
||||||
} else if (c == '"') {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Template literal state > ` <
|
|
||||||
case STATE_TEMPLATE_LITERAL:
|
|
||||||
if (c == "\\") {
|
|
||||||
state = STATE_ESCAPE_TEMPLATE_LITERAL;
|
|
||||||
} else if (c == "`") {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Single quote state > ' <
|
|
||||||
case STATE_QUOTE:
|
|
||||||
if (c == "\\") {
|
|
||||||
state = STATE_ESCAPE_QUOTE;
|
|
||||||
} else if (c == "\n") {
|
|
||||||
return {
|
|
||||||
err: "unterminated string literal",
|
|
||||||
};
|
|
||||||
} else if (c == "'") {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case STATE_SLASH:
|
|
||||||
if (c == "/") {
|
|
||||||
state = STATE_INLINE_COMMENT;
|
|
||||||
} else if (c == "*") {
|
|
||||||
state = STATE_MULTILINE_COMMENT;
|
|
||||||
} else {
|
|
||||||
lastStatement = "";
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STATE_INLINE_COMMENT:
|
|
||||||
if (c === "\n") {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
resetLastStatement = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STATE_MULTILINE_COMMENT:
|
|
||||||
if (c === "*") {
|
|
||||||
state = STATE_MULTILINE_COMMENT_CLOSE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STATE_MULTILINE_COMMENT_CLOSE:
|
|
||||||
if (c === "/") {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
resetLastStatement = true;
|
|
||||||
} else {
|
|
||||||
state = STATE_MULTILINE_COMMENT;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STATE_QUESTION_MARK:
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
if (c === "?") {
|
|
||||||
// If we have a nullish coalescing operator, we start a new statement
|
|
||||||
resetLastStatement = true;
|
|
||||||
} else if (c !== ".") {
|
|
||||||
// If we're not dealing with optional chaining (?.), it means we have a ternary,
|
|
||||||
// so we are starting a new statement that includes the current character.
|
|
||||||
lastStatement = "";
|
|
||||||
} else {
|
|
||||||
dotIndex = currentIndex;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isWhitespaceChar) {
|
|
||||||
previousNonWhitespaceChar = c;
|
|
||||||
}
|
|
||||||
if (resetLastStatement) {
|
|
||||||
lastStatement = "";
|
|
||||||
} else {
|
|
||||||
lastStatement = lastStatement + c;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We update all the open stacks lastStatement so they are up-to-date.
|
|
||||||
bodyStack.forEach(stack => {
|
|
||||||
if (stack.token !== "}") {
|
|
||||||
stack.lastStatement = stack.lastStatement + c;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let isElementAccess = false;
|
|
||||||
let lastOpeningBracketIndex = -1;
|
|
||||||
if (bodyStack.length === 1 && bodyStack[0].token === "[") {
|
|
||||||
lastStatement = bodyStack[0].lastStatement;
|
|
||||||
lastOpeningBracketIndex = bodyStack[0].index;
|
|
||||||
isElementAccess = true;
|
|
||||||
|
|
||||||
if (
|
|
||||||
state === STATE_DQUOTE ||
|
|
||||||
state === STATE_QUOTE ||
|
|
||||||
state === STATE_TEMPLATE_LITERAL ||
|
|
||||||
state === STATE_ESCAPE_QUOTE ||
|
|
||||||
state === STATE_ESCAPE_DQUOTE ||
|
|
||||||
state === STATE_ESCAPE_TEMPLATE_LITERAL
|
|
||||||
) {
|
|
||||||
state = STATE_NORMAL;
|
|
||||||
}
|
|
||||||
} else if (pendingWhitespaceChars) {
|
|
||||||
lastStatement = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastCompletionCharIndex = isElementAccess
|
|
||||||
? lastOpeningBracketIndex
|
|
||||||
: dotIndex;
|
|
||||||
|
|
||||||
const stringBeforeLastCompletionChar = str.slice(0, lastCompletionCharIndex);
|
|
||||||
|
|
||||||
const isPropertyAccess =
|
|
||||||
lastCompletionCharIndex && lastCompletionCharIndex > 0;
|
|
||||||
|
|
||||||
// Compute `isOptionalAccess`, so that we can use it
|
|
||||||
// later for computing `expressionBeforePropertyAccess`.
|
|
||||||
//Check `?.` before `[` for element access ( e.g `a?.["b` or `a ?. ["b` )
|
|
||||||
// and `?` before `.` for regular property access ( e.g `a?.b` or `a ?. b` )
|
|
||||||
const optionalElementAccessRegex = /\?\.\s*$/;
|
|
||||||
const isOptionalAccess = isElementAccess
|
|
||||||
? optionalElementAccessRegex.test(stringBeforeLastCompletionChar)
|
|
||||||
: isPropertyAccess &&
|
|
||||||
str.slice(lastCompletionCharIndex - 1, lastCompletionCharIndex + 1) ===
|
|
||||||
"?.";
|
|
||||||
|
|
||||||
// Get the filtered string for the properties (e.g if `document.qu` then `qu`)
|
|
||||||
const matchProp = isPropertyAccess
|
|
||||||
? str.slice(lastCompletionCharIndex + 1).trimLeft()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const expressionBeforePropertyAccess = isPropertyAccess
|
|
||||||
? str.slice(
|
|
||||||
0,
|
|
||||||
// For optional access, we can take all the chars before the last "?" char.
|
|
||||||
isOptionalAccess
|
|
||||||
? stringBeforeLastCompletionChar.lastIndexOf("?")
|
|
||||||
: lastCompletionCharIndex
|
|
||||||
)
|
|
||||||
: str;
|
|
||||||
|
|
||||||
let mainExpression = lastStatement;
|
|
||||||
if (isPropertyAccess) {
|
|
||||||
if (isOptionalAccess) {
|
|
||||||
// Strip anything before the last `?`.
|
|
||||||
mainExpression = mainExpression.slice(0, mainExpression.lastIndexOf("?"));
|
|
||||||
} else {
|
|
||||||
mainExpression = mainExpression.slice(
|
|
||||||
0,
|
|
||||||
-1 * (str.length - lastCompletionCharIndex)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainExpression = mainExpression.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
isElementAccess,
|
|
||||||
isPropertyAccess,
|
|
||||||
expressionBeforePropertyAccess,
|
|
||||||
lastStatement,
|
|
||||||
mainExpression,
|
|
||||||
matchProp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a given environment and constructor name, returns its Debugger.Object wrapped
|
* For a given environment and constructor name, returns its Debugger.Object wrapped
|
||||||
* prototype.
|
* prototype.
|
||||||
|
|
@ -1168,6 +787,3 @@ exports.JSPropertyProvider = DevToolsUtils.makeInfallible(JSPropertyProvider);
|
||||||
|
|
||||||
// Export a version that will throw (for tests)
|
// Export a version that will throw (for tests)
|
||||||
exports.FallibleJSPropertyProvider = JSPropertyProvider;
|
exports.FallibleJSPropertyProvider = JSPropertyProvider;
|
||||||
|
|
||||||
// Export analyzeInputString (for tests)
|
|
||||||
exports.analyzeInputString = analyzeInputString;
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ GeneratedFile(
|
||||||
)
|
)
|
||||||
|
|
||||||
DevToolsModules(
|
DevToolsModules(
|
||||||
|
"analyze-input-string.js",
|
||||||
"js-property-provider.js",
|
"js-property-provider.js",
|
||||||
"network-helper.js",
|
"network-helper.js",
|
||||||
"parser-helper.js",
|
"parser-helper.js",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
|
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
|
||||||
const {
|
const {
|
||||||
analyzeInputString,
|
analyzeInputString,
|
||||||
} = require("devtools/shared/webconsole/js-property-provider");
|
} = require("devtools/shared/webconsole/analyze-input-string");
|
||||||
|
|
||||||
add_task(() => {
|
add_task(() => {
|
||||||
const tests = [
|
const tests = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue