зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1594009 - Add optional chaining support for console autocomplete. r=Honza.
The JsPropertyProvider parser is modified to accept optional chaining syntax. Differential Revision: https://phabricator.services.mozilla.com/D60654 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
b84237cbb9
Коммит
54a45e09dd
|
@ -101,58 +101,30 @@ function JSPropertyProvider({
|
|||
|
||||
// Analyse the inputValue and find the beginning of the last part that
|
||||
// should be completed.
|
||||
const { err, state, lastStatement, isElementAccess } = analyzeInputString(
|
||||
inputValue
|
||||
);
|
||||
const inputAnalysis = analyzeInputString(inputValue);
|
||||
|
||||
// There was an error analysing the string.
|
||||
if (err) {
|
||||
console.error("Failed to analyze input string", err);
|
||||
if (!shouldInputBeAutocompleted(inputAnalysis)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 null;
|
||||
}
|
||||
|
||||
// Don't complete on just an empty string.
|
||||
if (lastStatement.trim() == "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
NO_AUTOCOMPLETE_PREFIXES.some(prefix =>
|
||||
lastStatement.startsWith(prefix + " ")
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const env = environment || dbgObject.asEnvironment();
|
||||
const completionPart = lastStatement;
|
||||
const lastDotIndex = completionPart.lastIndexOf(".");
|
||||
const lastOpeningBracketIndex = isElementAccess
|
||||
? completionPart.lastIndexOf("[")
|
||||
: -1;
|
||||
const lastCompletionCharIndex = Math.max(
|
||||
lastDotIndex,
|
||||
lastOpeningBracketIndex
|
||||
);
|
||||
const startQuoteRegex = /^('|"|`)/;
|
||||
let {
|
||||
mainExpression,
|
||||
matchProp,
|
||||
isPropertyAccess,
|
||||
} = splitInputAtLastPropertyAccess(inputAnalysis);
|
||||
const { lastStatement, isElementAccess } = inputAnalysis;
|
||||
|
||||
// AST representation of the expression before the last access char (`.` or `[`).
|
||||
let astExpression;
|
||||
let matchProp = completionPart.slice(lastCompletionCharIndex + 1).trimLeft();
|
||||
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 && lastCompletionCharIndex > 0) {
|
||||
const parsedExpression = completionPart.slice(0, lastCompletionCharIndex);
|
||||
const syntaxTrees = getSyntaxTrees(parsedExpression);
|
||||
if (!isWorker && isPropertyAccess) {
|
||||
const syntaxTrees = getSyntaxTrees(mainExpression);
|
||||
const lastTree = syntaxTrees[syntaxTrees.length - 1];
|
||||
const lastBody = lastTree && lastTree.body[lastTree.body.length - 1];
|
||||
|
||||
|
@ -180,7 +152,7 @@ function JSPropertyProvider({
|
|||
// (but the following are fine: `1..`, `(1.).`).
|
||||
if (
|
||||
!Number.isInteger(astExpression.value) ||
|
||||
/\d[^\.]{0}\.$/.test(completionPart) === false
|
||||
/\d[^\.]{0}\.$/.test(lastStatement) === false
|
||||
) {
|
||||
matchingObject = getContentPrototypeObject(env, "Number");
|
||||
} else {
|
||||
|
@ -215,7 +187,7 @@ function JSPropertyProvider({
|
|||
let properties = [];
|
||||
|
||||
if (astExpression) {
|
||||
if (lastCompletionCharIndex > -1) {
|
||||
if (isPropertyAccess) {
|
||||
properties = getPropertiesFromAstExpression(astExpression);
|
||||
|
||||
if (properties === null) {
|
||||
|
@ -223,7 +195,7 @@ function JSPropertyProvider({
|
|||
}
|
||||
}
|
||||
} else {
|
||||
properties = completionPart.split(".");
|
||||
properties = lastStatement.split(".");
|
||||
if (isElementAccess) {
|
||||
const lastPart = properties[properties.length - 1];
|
||||
const openBracketIndex = lastPart.lastIndexOf("[");
|
||||
|
@ -362,6 +334,93 @@ function JSPropertyProvider({
|
|||
return prepareReturnedObject(getMatchedPropsInDbgObject(obj, search));
|
||||
}
|
||||
|
||||
function shouldInputBeAutocompleted(inputAnalysisState) {
|
||||
const { err, state, lastStatement } = inputAnalysisState;
|
||||
|
||||
// There was an error analysing the string.
|
||||
if (err) {
|
||||
console.error("Failed to analyze input string", 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 that will process the result of analyzeInputString and return useful information
|
||||
* about the expression.
|
||||
*
|
||||
* @param {Object} inputAnalysisState: Result of analyzeInputString.
|
||||
* @returns {Object} An object of the following shape:
|
||||
* - mainExpression: The part of the expression before any property access,
|
||||
* (e.g. `a.b` if expression is `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`)
|
||||
* - isPropertyAccess: true of the expression indicate that the
|
||||
* expression has a property access (e.g. `a.x` or `a["x"`).
|
||||
*/
|
||||
function splitInputAtLastPropertyAccess(inputAnalysisState) {
|
||||
const { lastStatement, isElementAccess } = inputAnalysisState;
|
||||
const lastDotIndex = lastStatement.lastIndexOf(".");
|
||||
const lastOpeningBracketIndex = isElementAccess
|
||||
? lastStatement.lastIndexOf("[")
|
||||
: -1;
|
||||
const lastCompletionCharIndex = Math.max(
|
||||
lastDotIndex,
|
||||
lastOpeningBracketIndex
|
||||
);
|
||||
|
||||
const matchProp = lastStatement.slice(lastCompletionCharIndex + 1).trimLeft();
|
||||
const matchPropPrefix = lastStatement.slice(0, lastCompletionCharIndex + 1);
|
||||
|
||||
const optionalChainingElementAccessRegex = /\s*\?\.\s*\[\s*$/;
|
||||
const isPropertyAccess = lastCompletionCharIndex > 0;
|
||||
|
||||
let mainExpression;
|
||||
if (!isPropertyAccess) {
|
||||
mainExpression = matchProp;
|
||||
} else if (
|
||||
isElementAccess &&
|
||||
optionalChainingElementAccessRegex.test(matchPropPrefix)
|
||||
) {
|
||||
// Strip the optional chaining operator at the end;
|
||||
mainExpression = matchPropPrefix.replace(
|
||||
optionalChainingElementAccessRegex,
|
||||
""
|
||||
);
|
||||
} else if (!isElementAccess && matchPropPrefix.endsWith("?.")) {
|
||||
mainExpression = matchPropPrefix.slice(0, matchPropPrefix.length - 2);
|
||||
} else {
|
||||
mainExpression = matchPropPrefix.slice(0, matchPropPrefix.length - 1);
|
||||
}
|
||||
mainExpression = mainExpression.trim();
|
||||
|
||||
return {
|
||||
mainExpression,
|
||||
matchProp,
|
||||
isPropertyAccess,
|
||||
};
|
||||
}
|
||||
|
||||
function hasArrayIndex(str) {
|
||||
return /\[\d+\]$/.test(str);
|
||||
}
|
||||
|
@ -375,6 +434,7 @@ 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("");
|
||||
|
@ -385,7 +445,7 @@ const OPEN_CLOSE_BODY = {
|
|||
};
|
||||
|
||||
const NO_AUTOCOMPLETE_PREFIXES = ["var", "const", "let", "function", "class"];
|
||||
const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~?!".split(""));
|
||||
const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~!".split(""));
|
||||
|
||||
/**
|
||||
* Analyses a given string to find the last statement that is interesting for
|
||||
|
@ -438,10 +498,17 @@ function analyzeInputString(str) {
|
|||
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 = "";
|
||||
}
|
||||
|
||||
// If the last characters were spaces, and the current one is not.
|
||||
if (pendingWhitespaceChars && !isWhitespaceChar) {
|
||||
// If we have a legitimate property/element access, we append the spaces.
|
||||
if (c === "[" || c === ".") {
|
||||
// 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.
|
||||
|
@ -458,6 +525,8 @@ function analyzeInputString(str) {
|
|||
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;
|
||||
|
@ -568,6 +637,18 @@ function analyzeInputString(str) {
|
|||
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 = "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isWhitespaceChar) {
|
||||
|
@ -644,12 +725,17 @@ function getPropertiesFromAstExpression(ast) {
|
|||
if (!ast) {
|
||||
return result;
|
||||
}
|
||||
const { type, property, object, name } = ast;
|
||||
const { type, property, object, name, expression } = ast;
|
||||
if (type === "ThisExpression") {
|
||||
result.unshift("this");
|
||||
} else if (type === "Identifier" && name) {
|
||||
result.unshift(name);
|
||||
} else if (type === "MemberExpression") {
|
||||
} 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);
|
||||
|
|
|
@ -594,6 +594,70 @@ function runChecks(dbgObject, environment, sandbox) {
|
|||
});
|
||||
test_has_result(results, "'propA'");
|
||||
Assert.equal(results.matches.has("propLocal"), false);
|
||||
|
||||
info("Test that expression with optional chaining operator are completed");
|
||||
results = propertyProvider("testObject?.prop");
|
||||
test_has_result(results, "propA");
|
||||
|
||||
results = propertyProvider("testObject?.propA[0]?.propB?.to");
|
||||
test_has_result(results, "toString");
|
||||
|
||||
results = propertyProvider("testObject?.propA?.[0]?.propB?.to");
|
||||
test_has_result(results, "toString");
|
||||
|
||||
results = propertyProvider(
|
||||
"testObject ?. propA[0] ?. propB ?. to"
|
||||
);
|
||||
test_has_result(results, "toString");
|
||||
|
||||
results = propertyProvider("testObject?.[prop");
|
||||
test_has_result(results, '"propA"');
|
||||
|
||||
results = propertyProvider(`testObject?.["prop`);
|
||||
test_has_result(results, '"propA"');
|
||||
|
||||
results = propertyProvider(`testObject?.['prop`);
|
||||
test_has_result(results, `'propA'`);
|
||||
|
||||
results = propertyProvider(`testObject?.["propA"]?.[0]?.["propB"]?.["to`);
|
||||
test_has_result(results, `"toString"`);
|
||||
|
||||
results = propertyProvider(
|
||||
`testObject ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`
|
||||
);
|
||||
test_has_result(results, "'toString'");
|
||||
|
||||
results = propertyProvider("[1,2,3]?.");
|
||||
test_has_result(results, "indexOf");
|
||||
|
||||
results = propertyProvider("'foo'?.");
|
||||
test_has_result(results, "charAt");
|
||||
|
||||
results = propertyProvider("1?.");
|
||||
test_has_result(results, "toFixed");
|
||||
|
||||
// check this doesn't throw since `propC` is not defined.
|
||||
results = propertyProvider("testObject?.propC?.this?.does?.not?.exist?.d");
|
||||
|
||||
// check that ternary operator isn't mistaken for optional chaining
|
||||
results = propertyProvider(`true?.3.to`);
|
||||
test_has_result(results, `toExponential`);
|
||||
|
||||
results = propertyProvider(`true?.3?.to`);
|
||||
test_has_result(results, `toExponential`);
|
||||
|
||||
// Test more ternary
|
||||
results = propertyProvider(`true?t`);
|
||||
test_has_result(results, `testObject`);
|
||||
|
||||
results = propertyProvider(`true??t`);
|
||||
test_has_result(results, `testObject`);
|
||||
|
||||
results = propertyProvider(`true?/* comment */t`);
|
||||
test_has_result(results, `testObject`);
|
||||
|
||||
results = propertyProvider(`true?<t`);
|
||||
test_has_no_results(results);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -615,7 +679,12 @@ function test_has_no_results(results) {
|
|||
function test_has_result(results, requiredSuggestion) {
|
||||
Assert.notEqual(results, null);
|
||||
Assert.ok(results.matches.size > 0);
|
||||
Assert.ok(results.matches.has(requiredSuggestion));
|
||||
Assert.ok(
|
||||
results.matches.has(requiredSuggestion),
|
||||
`<${requiredSuggestion}> found in ${[...results.matches.values()].join(
|
||||
" - "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Загрузка…
Ссылка в новой задаче