/* 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/. */
// 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,
frameActorId,
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)) { returnnull;
}
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, frameActor: frameActorId },
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);
// 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) { returnnull;
}
astExpression = lastBody.expression;
let matchingObject;
if (astExpression.type === "ArrayExpression") {
matchingObject = getContentPrototypeObject(env, "Array");
} elseif (
astExpression.type === "Literal" && typeof astExpression.value === "string"
) {
matchingObject = getContentPrototypeObject(env, "String");
} elseif (
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 { returnnull;
}
}
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);
}
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.
}
} elseif (firstProp === "$_" && webconsoleActor) {
obj = webconsoleActor.getLastConsoleInputEvaluation();
} elseif (firstProp === "$0" && selectedNodeActor && webconsoleActor) { const actor = webconsoleActor.conn.getActor(selectedNodeActor); if (actor) { try {
obj = webconsoleActor.makeDebuggeeValue(actor.rawNode);
} catch (e) { // Ignore.
}
}
} elseif (hasArrayIndex(firstProp)) {
obj = getArrayMemberProperty(null, env, firstProp);
} else {
obj = getVariableInEnvironment(env, firstProp);
}
if (!isObjectUsable(obj)) { returnnull;
}
// 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();
}
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);
}
function hasArrayIndex(str) { return /\[\d+\]$/.test(str);
}
/** * 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) { returnnull;
}
/** * @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");
} elseif (type === "Identifier" && name) {
result.unshift(name);
} elseif (type === "OptionalExpression" && expression) {
result = (getPropertiesFromAstExpression(expression) || []).concat(result);
} elseif (
type === "MemberExpression" ||
type === "OptionalMemberExpression"
) { if (property) { if (property.type === "Identifier" && property.name) {
result.unshift(property.name);
} elseif (property.type === "Literal") {
result.unshift(property.value);
}
} if (object) {
result = (getPropertiesFromAstExpression(object) || []).concat(result);
}
} else { returnnull;
} return result;
}
function wrapMatchesInQuotes(matches, quote = `"`) { returnnew 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("["));
// 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)) { returnnull;
}
obj = DevToolsUtils.getProperty(obj, index);
if (!isObjectUsable(obj)) { returnnull;
}
}
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) { returnfalse;
}
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);
} elseif (!isWorker) { // If we're not performing an element access, we need to check that the property // are suited for a dot access. (reflect.sys.mjs 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.
lazy.Reflect.parse(`({${match}: true})`);
} catch (e) {
matches.delete(match);
}
}
}
/** * 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;
// 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 || prop === "Infinity") {
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(obj) { while (obj) {
yield obj; try {
obj = Object.getPrototypeOf(obj);
} catch (error) { // The above can throw e.g. for some proxy objects. return;
}
}
},
getProperties(obj) { try { return Object.getOwnPropertyNames(obj);
} catch (error) { // The above can throw e.g. for some proxy objects. returnnull;
}
},
getProperty() { // getProperty is unsafe with raw JS objects. thrownew Error("Unimplemented!");
},
};
var DebuggerObjectSupport = {
*chainIterator(obj) { while (obj) {
yield obj; try { // There could be transparent security wrappers, unwrap to check if it's a proxy. const unwrapped = DevToolsUtils.unwrap(obj); if (unwrapped === undefined) { // Objects belonging to an invisible-to-debugger compartment can't be unwrapped. return;
}
if (unwrapped.isProxy) { // Proxies might have a `getPrototypeOf` method, which is triggered by `obj.proto`, // but this does not impact the actual prototype chain. // In such case, we need to use the proxy target prototype. // We retrieve proxyTarget from `obj` (and not `unwrapped`) to avoid exposing // the unwrapped target.
obj = unwrapped.proxyTarget;
}
obj = obj.proto;
} catch (error) { // The above can throw e.g. for some proxy objects. return;
}
}
},
getProperties(obj) { try { return obj.getOwnPropertyNames();
} catch (error) { // The above can throw e.g. for some proxy objects. returnnull;
}
},
getProperty() { // This is left unimplemented in favor to DevToolsUtils.getProperty(). thrownew Error("Unimplemented!");
},
};
var DebuggerEnvironmentSupport = {
*chainIterator(obj) { while (obj) {
yield obj;
obj = obj.parent;
}
},
getProperties(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(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))
) { returnnull;
} return { value: result };
},
};
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.