summaryrefslogtreecommitdiffstats
path: root/devtools/shared/webconsole/js-property-provider.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/webconsole/js-property-provider.js')
-rw-r--r--devtools/shared/webconsole/js-property-provider.js1173
1 files changed, 1173 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/js-property-provider.js b/devtools/shared/webconsole/js-property-provider.js
new file mode 100644
index 0000000000..7376ea6c9b
--- /dev/null
+++ b/devtools/shared/webconsole/js-property-provider.js
@@ -0,0 +1,1173 @@
+/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+const {
+ evalWithDebugger,
+} = require("devtools/server/actors/webconsole/eval-with-debugger");
+
+if (!isWorker) {
+ loader.lazyRequireGetter(
+ this,
+ "getSyntaxTrees",
+ "devtools/shared/webconsole/parser-helper",
+ true
+ );
+}
+loader.lazyRequireGetter(
+ this,
+ "Reflect",
+ "resource://gre/modules/reflect.jsm",
+ true
+);
+
+// Provide an easy way to bail out of even attempting an autocompletion
+// if an object has way too many properties. Protects against large objects
+// with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
+const MAX_AUTOCOMPLETE_ATTEMPTS = (exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000);
+// Prevent iterating over too many properties during autocomplete suggestions.
+const MAX_AUTOCOMPLETIONS = (exports.MAX_AUTOCOMPLETIONS = 1500);
+
+/**
+ * Provides a list of properties, that are possible matches based on the passed
+ * Debugger.Environment/Debugger.Object and inputValue.
+ *
+ * @param {Object} An object of the following shape:
+ * - {Object} dbgObject
+ * When the debugger is not paused this Debugger.Object wraps
+ * the scope for autocompletion.
+ * It is null if the debugger is paused.
+ * - {Object} environment
+ * When the debugger is paused this Debugger.Environment is the
+ * scope for autocompletion.
+ * It is null if the debugger is not paused.
+ * - {String} inputValue
+ * Value that should be completed.
+ * - {Number} cursor (defaults to inputValue.length).
+ * Optional offset in the input where the cursor is located. If this is
+ * omitted then the cursor is assumed to be at the end of the input
+ * value.
+ * - {Array} authorizedEvaluations (defaults to []).
+ * Optional array containing all the different properties access that the engine
+ * can execute in order to retrieve its result's properties.
+ * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects
+ * in the content page ⚠️
+ * - {WebconsoleActor} webconsoleActor
+ * A reference to a webconsole actor which we can use to retrieve the last
+ * evaluation result or create a debuggee value.
+ * - {String}: selectedNodeActor
+ * The actor id of the selected node in the inspector.
+ * - {Array<string>}: expressionVars
+ * Optional array containing variable defined in the expression. Those variables
+ * are extracted from CodeMirror state.
+ * @returns null or object
+ * If the inputValue is an unsafe getter and invokeUnsafeGetter is false, the
+ * following form is returned:
+ *
+ * {
+ * isUnsafeGetter: true,
+ * getterPath: {Array<String>} An array of the property chain leading to the
+ * getter. Example: ["x", "myGetter"]
+ * }
+ *
+ * If no completion valued could be computed, and the input is not an unsafe
+ * getter, null is returned.
+ *
+ * Otherwise an object with the following form is returned:
+ * {
+ * matches: Set<string>
+ * matchProp: Last part of the inputValue that was used to find
+ * the matches-strings.
+ * isElementAccess: Boolean set to true if the evaluation is an element
+ * access (e.g. `window["addEvent`).
+ * }
+ */
+// eslint-disable-next-line complexity
+function JSPropertyProvider({
+ dbgObject,
+ environment,
+ inputValue,
+ cursor,
+ authorizedEvaluations = [],
+ webconsoleActor,
+ selectedNodeActor,
+ expressionVars = [],
+}) {
+ if (cursor === undefined) {
+ cursor = inputValue.length;
+ }
+
+ inputValue = inputValue.substring(0, cursor);
+
+ // Analyse the inputValue and find the beginning of the last part that
+ // should be completed.
+ const inputAnalysis = analyzeInputString(inputValue);
+
+ if (!shouldInputBeAutocompleted(inputAnalysis)) {
+ return null;
+ }
+
+ let {
+ lastStatement,
+ isElementAccess,
+ mainExpression,
+ matchProp,
+ isPropertyAccess,
+ } = inputAnalysis;
+
+ // Eagerly evaluate the main expression and return the results properties.
+ // e.g. `obj.func().a` will evaluate `obj.func()` and return properties matching `a`.
+ // NOTE: this is only useful when the input has a property access.
+ if (webconsoleActor && shouldInputBeEagerlyEvaluated(inputAnalysis)) {
+ const eagerResponse = evalWithDebugger(
+ mainExpression,
+ { eager: true, selectedNodeActor },
+ webconsoleActor
+ );
+
+ const ret = eagerResponse?.result?.return;
+
+ // Only send matches if eager evaluation returned something meaningful
+ if (ret && ret !== undefined) {
+ const matches =
+ typeof ret != "object"
+ ? getMatchedProps(ret, matchProp)
+ : getMatchedPropsInDbgObject(ret, matchProp);
+
+ return prepareReturnedObject({
+ matches,
+ search: matchProp,
+ isElementAccess,
+ });
+ }
+ }
+
+ // AST representation of the expression before the last access char (`.` or `[`).
+ let astExpression;
+ const startQuoteRegex = /^('|"|`)/;
+ const env = environment || dbgObject.asEnvironment();
+
+ // Catch literals like [1,2,3] or "foo" and return the matches from
+ // their prototypes.
+ // Don't run this is a worker, migrating to acorn should allow this
+ // to run in a worker - Bug 1217198.
+ if (!isWorker && isPropertyAccess) {
+ const syntaxTrees = getSyntaxTrees(mainExpression);
+ const lastTree = syntaxTrees[syntaxTrees.length - 1];
+ const lastBody = lastTree?.body[lastTree.body.length - 1];
+
+ // Finding the last expression since we've sliced up until the dot.
+ // If there were parse errors this won't exist.
+ if (lastBody) {
+ if (!lastBody.expression) {
+ return null;
+ }
+
+ astExpression = lastBody.expression;
+ let matchingObject;
+
+ if (astExpression.type === "ArrayExpression") {
+ matchingObject = getContentPrototypeObject(env, "Array");
+ } else if (
+ astExpression.type === "Literal" &&
+ typeof astExpression.value === "string"
+ ) {
+ matchingObject = getContentPrototypeObject(env, "String");
+ } else if (
+ astExpression.type === "Literal" &&
+ Number.isFinite(astExpression.value)
+ ) {
+ // The parser rightfuly indicates that we have a number in some cases (e.g. `1.`),
+ // but we don't want to return Number proto properties in that case since
+ // the result would be invalid (i.e. `1.toFixed()` throws).
+ // So if the expression value is an integer, it should not end with `{Number}.`
+ // (but the following are fine: `1..`, `(1.).`).
+ if (
+ !Number.isInteger(astExpression.value) ||
+ /\d[^\.]{0}\.$/.test(lastStatement) === false
+ ) {
+ matchingObject = getContentPrototypeObject(env, "Number");
+ } else {
+ return null;
+ }
+ }
+
+ if (matchingObject) {
+ let search = matchProp;
+
+ let elementAccessQuote;
+ if (isElementAccess && startQuoteRegex.test(matchProp)) {
+ elementAccessQuote = matchProp[0];
+ search = matchProp.replace(startQuoteRegex, "");
+ }
+
+ let props = getMatchedPropsInDbgObject(matchingObject, search);
+
+ if (isElementAccess) {
+ props = wrapMatchesInQuotes(props, elementAccessQuote);
+ }
+
+ return {
+ isElementAccess,
+ matchProp,
+ matches: props,
+ };
+ }
+ }
+ }
+
+ // We are completing a variable / a property lookup.
+ let properties = [];
+
+ if (astExpression) {
+ if (isPropertyAccess) {
+ properties = getPropertiesFromAstExpression(astExpression);
+
+ if (properties === null) {
+ return null;
+ }
+ }
+ } else {
+ properties = lastStatement.split(".");
+ if (isElementAccess) {
+ const lastPart = properties[properties.length - 1];
+ const openBracketIndex = lastPart.lastIndexOf("[");
+ matchProp = lastPart.substr(openBracketIndex + 1);
+ properties[properties.length - 1] = lastPart.substring(
+ 0,
+ openBracketIndex
+ );
+ } else {
+ matchProp = properties.pop().trimLeft();
+ }
+ }
+
+ let search = matchProp;
+ let elementAccessQuote;
+ if (isElementAccess && startQuoteRegex.test(search)) {
+ elementAccessQuote = search[0];
+ search = search.replace(startQuoteRegex, "");
+ }
+
+ let obj = dbgObject;
+ if (properties.length === 0) {
+ const environmentProperties = getMatchedPropsInEnvironment(env, search);
+ const expressionVariables = new Set(
+ expressionVars.filter(variableName => variableName.startsWith(matchProp))
+ );
+
+ for (const prop of environmentProperties) {
+ expressionVariables.add(prop);
+ }
+
+ return {
+ isElementAccess,
+ matchProp,
+ matches: expressionVariables,
+ };
+ }
+
+ let firstProp = properties.shift();
+ if (typeof firstProp == "string") {
+ firstProp = firstProp.trim();
+ }
+
+ if (firstProp === "this") {
+ // Special case for 'this' - try to get the Object from the Environment.
+ // No problem if it throws, we will just not autocomplete.
+ try {
+ obj = env.object;
+ } catch (e) {
+ // Ignore.
+ }
+ } else if (firstProp === "$_" && webconsoleActor) {
+ obj = webconsoleActor.getLastConsoleInputEvaluation();
+ } else if (firstProp === "$0" && selectedNodeActor && webconsoleActor) {
+ const actor = webconsoleActor.conn.getActor(selectedNodeActor);
+ if (actor) {
+ try {
+ obj = webconsoleActor.makeDebuggeeValue(actor.rawNode);
+ } catch (e) {
+ // Ignore.
+ }
+ }
+ } else if (hasArrayIndex(firstProp)) {
+ obj = getArrayMemberProperty(null, env, firstProp);
+ } else {
+ obj = getVariableInEnvironment(env, firstProp);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+
+ // We get the rest of the properties recursively starting from the
+ // Debugger.Object that wraps the first property
+ for (let [index, prop] of properties.entries()) {
+ if (typeof prop === "string") {
+ prop = prop.trim();
+ }
+
+ if (prop === undefined || prop === null || prop === "") {
+ return null;
+ }
+
+ const propPath = [firstProp].concat(properties.slice(0, index + 1));
+ const authorized = authorizedEvaluations.some(
+ x => JSON.stringify(x) === JSON.stringify(propPath)
+ );
+
+ if (!authorized && DevToolsUtils.isUnsafeGetter(obj, prop)) {
+ // If we try to access an unsafe getter, return its name so we can consume that
+ // on the frontend.
+ return {
+ isUnsafeGetter: true,
+ getterPath: propPath,
+ };
+ }
+
+ if (hasArrayIndex(prop)) {
+ // The property to autocomplete is a member of array. For example
+ // list[i][j]..[n]. Traverse the array to get the actual element.
+ obj = getArrayMemberProperty(obj, null, prop);
+ } else {
+ obj = DevToolsUtils.getProperty(obj, prop, authorized);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+ }
+
+ const matches =
+ typeof obj != "object"
+ ? getMatchedProps(obj, search)
+ : getMatchedPropsInDbgObject(obj, search);
+ return prepareReturnedObject({
+ matches,
+ search,
+ isElementAccess,
+ elementAccessQuote,
+ });
+}
+
+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) {
+ 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
+ * prototype.
+ *
+ * @param {Environment} env
+ * @param {String} name: Name of the constructor object we want the prototype of.
+ * @returns {Debugger.Object|null} the prototype, or null if it not found.
+ */
+function getContentPrototypeObject(env, name) {
+ // Retrieve the outermost environment to get the global object.
+ let outermostEnv = env;
+ while (outermostEnv?.parent) {
+ outermostEnv = outermostEnv.parent;
+ }
+
+ const constructorObj = DevToolsUtils.getProperty(outermostEnv.object, name);
+ if (!constructorObj) {
+ return null;
+ }
+
+ return DevToolsUtils.getProperty(constructorObj, "prototype");
+}
+
+/**
+ * @param {Object} ast: An AST representing a property access (e.g. `foo.bar["baz"].x`)
+ * @returns {Array|null} An array representing the property access
+ * (e.g. ["foo", "bar", "baz", "x"]).
+ */
+function getPropertiesFromAstExpression(ast) {
+ let result = [];
+ if (!ast) {
+ return result;
+ }
+ const { type, property, object, name, expression } = ast;
+ if (type === "ThisExpression") {
+ result.unshift("this");
+ } else if (type === "Identifier" && name) {
+ result.unshift(name);
+ } else if (type === "OptionalExpression" && expression) {
+ result = (getPropertiesFromAstExpression(expression) || []).concat(result);
+ } else if (
+ type === "MemberExpression" ||
+ type === "OptionalMemberExpression"
+ ) {
+ if (property) {
+ if (property.type === "Identifier" && property.name) {
+ result.unshift(property.name);
+ } else if (property.type === "Literal") {
+ result.unshift(property.value);
+ }
+ }
+ if (object) {
+ result = (getPropertiesFromAstExpression(object) || []).concat(result);
+ }
+ } else {
+ return null;
+ }
+ return result;
+}
+
+function wrapMatchesInQuotes(matches, quote = `"`) {
+ return new Set(
+ [...matches].map(p => {
+ // Escape as a double-quoted string literal
+ p = JSON.stringify(p);
+
+ // We don't have to do anything more when using double quotes
+ if (quote == `"`) {
+ return p;
+ }
+
+ // Remove surrounding double quotes
+ p = p.slice(1, -1);
+
+ // Unescape inner double quotes (all must be escaped, so no need to count backslashes)
+ p = p.replace(/\\(?=")/g, "");
+
+ // Escape the specified quote (assuming ' or `, which are treated literally in regex)
+ p = p.replace(new RegExp(quote, "g"), "\\$&");
+
+ // Template literals treat ${ specially, escape it
+ if (quote == "`") {
+ p = p.replace(/\${/g, "\\$&");
+ }
+
+ // Surround the result with quotes
+ return `${quote}${p}${quote}`;
+ })
+ );
+}
+
+/**
+ * Get the array member of obj for the given prop. For example, given
+ * prop='list[0][1]' the element at [0][1] of obj.list is returned.
+ *
+ * @param object obj
+ * The object to operate on. Should be null if env is passed.
+ * @param object env
+ * The Environment to operate in. Should be null if obj is passed.
+ * @param string prop
+ * The property to return.
+ * @return null or Object
+ * Returns null if the property couldn't be located. Otherwise the array
+ * member identified by prop.
+ */
+function getArrayMemberProperty(obj, env, prop) {
+ // First get the array.
+ const propWithoutIndices = prop.substr(0, prop.indexOf("["));
+
+ if (env) {
+ obj = getVariableInEnvironment(env, propWithoutIndices);
+ } else {
+ obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+
+ // Then traverse the list of indices to get the actual element.
+ let result;
+ const arrayIndicesRegex = /\[[^\]]*\]/g;
+ while ((result = arrayIndicesRegex.exec(prop)) !== null) {
+ const indexWithBrackets = result[0];
+ const indexAsText = indexWithBrackets.substr(
+ 1,
+ indexWithBrackets.length - 2
+ );
+ const index = parseInt(indexAsText, 10);
+
+ if (isNaN(index)) {
+ return null;
+ }
+
+ obj = DevToolsUtils.getProperty(obj, index);
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+ }
+
+ return obj;
+}
+
+/**
+ * Check if the given Debugger.Object can be used for autocomplete.
+ *
+ * @param Debugger.Object object
+ * The Debugger.Object to check.
+ * @return boolean
+ * True if further inspection into the object is possible, or false
+ * otherwise.
+ */
+function isObjectUsable(object) {
+ if (object == null) {
+ return false;
+ }
+
+ if (typeof object == "object" && object.class == "DeadObject") {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @see getExactMatchImpl()
+ */
+function getVariableInEnvironment(environment, name) {
+ return getExactMatchImpl(environment, name, DebuggerEnvironmentSupport);
+}
+
+function prepareReturnedObject({
+ matches,
+ search,
+ isElementAccess,
+ elementAccessQuote,
+}) {
+ if (isElementAccess) {
+ // If it's an element access, we need to wrap properties in quotes (either the one
+ // the user already typed, or `"`).
+
+ matches = wrapMatchesInQuotes(matches, elementAccessQuote);
+ } else if (!isWorker) {
+ // If we're not performing an element access, we need to check that the property
+ // are suited for a dot access. (Reflect.jsm is not available in worker context yet,
+ // see Bug 1507181).
+ for (const match of matches) {
+ try {
+ // In order to know if the property is suited for dot notation, we use Reflect
+ // to parse an expression where we try to access the property with a dot. If it
+ // throws, this means that we need to do an element access instead.
+ Reflect.parse(`({${match}: true})`);
+ } catch (e) {
+ matches.delete(match);
+ }
+ }
+ }
+
+ return { isElementAccess, matchProp: search, matches };
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedPropsInEnvironment(environment, match) {
+ return getMatchedPropsImpl(environment, match, DebuggerEnvironmentSupport);
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedPropsInDbgObject(dbgObject, match) {
+ return getMatchedPropsImpl(dbgObject, match, DebuggerObjectSupport);
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedProps(obj, match) {
+ if (typeof obj != "object") {
+ obj = obj.constructor.prototype;
+ }
+ return getMatchedPropsImpl(obj, match, JSObjectSupport);
+}
+
+/**
+ * Get all properties in the given object (and its parent prototype chain) that
+ * match a given prefix.
+ *
+ * @param {Mixed} obj
+ * Object whose properties we want to filter.
+ * @param {string} match
+ * Filter for properties that match this string.
+ * @returns {Set} List of matched properties.
+ */
+function getMatchedPropsImpl(obj, match, { chainIterator, getProperties }) {
+ const matches = new Set();
+ let numProps = 0;
+
+ const insensitiveMatching = match && match[0].toUpperCase() !== match[0];
+ const propertyMatches = prop => {
+ return insensitiveMatching
+ ? prop.toLocaleLowerCase().startsWith(match.toLocaleLowerCase())
+ : prop.startsWith(match);
+ };
+
+ // We need to go up the prototype chain.
+ const iter = chainIterator(obj);
+ for (obj of iter) {
+ const props = getProperties(obj);
+ if (!props) {
+ continue;
+ }
+ numProps += props.length;
+
+ // If there are too many properties to event attempt autocompletion,
+ // or if we have already added the max number, then stop looping
+ // and return the partial set that has already been discovered.
+ if (
+ numProps >= MAX_AUTOCOMPLETE_ATTEMPTS ||
+ matches.size >= MAX_AUTOCOMPLETIONS
+ ) {
+ break;
+ }
+
+ for (let i = 0; i < props.length; i++) {
+ const prop = props[i];
+ if (!propertyMatches(prop)) {
+ continue;
+ }
+
+ // If it is an array index, we can't take it.
+ // This uses a trick: converting a string to a number yields NaN if
+ // the operation failed, and NaN is not equal to itself.
+ // eslint-disable-next-line no-self-compare
+ if (+prop != +prop) {
+ matches.add(prop);
+ }
+
+ if (matches.size >= MAX_AUTOCOMPLETIONS) {
+ break;
+ }
+ }
+ }
+
+ return matches;
+}
+
+/**
+ * Returns a property value based on its name from the given object, by
+ * recursively checking the object's prototype.
+ *
+ * @param object obj
+ * An object to look the property into.
+ * @param string name
+ * The property that is looked up.
+ * @returns object|undefined
+ * A Debugger.Object if the property exists in the object's prototype
+ * chain, undefined otherwise.
+ */
+function getExactMatchImpl(obj, name, { chainIterator, getProperty }) {
+ // We need to go up the prototype chain.
+ const iter = chainIterator(obj);
+ for (obj of iter) {
+ const prop = getProperty(obj, name, obj);
+ if (prop) {
+ return prop.value;
+ }
+ }
+ return undefined;
+}
+
+var JSObjectSupport = {
+ chainIterator: function*(obj) {
+ while (obj) {
+ yield obj;
+ try {
+ obj = Object.getPrototypeOf(obj);
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return;
+ }
+ }
+ },
+
+ getProperties: function(obj) {
+ try {
+ return Object.getOwnPropertyNames(obj);
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return null;
+ }
+ },
+
+ getProperty: function() {
+ // getProperty is unsafe with raw JS objects.
+ throw new Error("Unimplemented!");
+ },
+};
+
+var DebuggerObjectSupport = {
+ chainIterator: function*(obj) {
+ while (obj) {
+ yield obj;
+ try {
+ obj = obj.proto;
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return;
+ }
+ }
+ },
+
+ getProperties: function(obj) {
+ try {
+ return obj.getOwnPropertyNames();
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return null;
+ }
+ },
+
+ getProperty: function(obj, name, rootObj) {
+ // This is left unimplemented in favor to DevToolsUtils.getProperty().
+ throw new Error("Unimplemented!");
+ },
+};
+
+var DebuggerEnvironmentSupport = {
+ chainIterator: function*(obj) {
+ while (obj) {
+ yield obj;
+ obj = obj.parent;
+ }
+ },
+
+ getProperties: function(obj) {
+ const names = obj.names();
+
+ // Include 'this' in results (in sorted order)
+ for (let i = 0; i < names.length; i++) {
+ if (i === names.length - 1 || names[i + 1] > "this") {
+ names.splice(i + 1, 0, "this");
+ break;
+ }
+ }
+
+ return names;
+ },
+
+ getProperty: function(obj, name) {
+ let result;
+ // Try/catch since name can be anything, and getVariable throws if
+ // it's not a valid ECMAScript identifier name
+ try {
+ // TODO: we should use getVariableDescriptor() here - bug 725815.
+ result = obj.getVariable(name);
+ } catch (e) {
+ // Ignore.
+ }
+
+ // FIXME: Need actual UI, bug 941287.
+ if (
+ result == null ||
+ (typeof result == "object" &&
+ (result.optimizedOut || result.missingArguments))
+ ) {
+ return null;
+ }
+ return { value: result };
+ },
+};
+
+exports.JSPropertyProvider = DevToolsUtils.makeInfallible(JSPropertyProvider);
+
+// Export a version that will throw (for tests)
+exports.FallibleJSPropertyProvider = JSPropertyProvider;
+
+// Export analyzeInputString (for tests)
+exports.analyzeInputString = analyzeInputString;