Skip to content

Commit

Permalink
@babel/parser error recovery (#10363)
Browse files Browse the repository at this point in the history
* Add error recovery support to @babel/parser

* Update @babel/parser tests to always recover from errors

* Update this.raise usage in @babel/parser:

- expression.js
- lval.js
- statement.js
- estree.js
- flow.js
- jsx/index.js
- tokenizer/index.js

* Update @babel/parser fixtures with recovered errors

* Fix tests out of @babel/parser

* Do not use try/catch for control flow

* Update invalid fixtures

* Do not report invalid lhs in toAssignable

* Do not validate function id multiple times

* Dedupe reserved await errors

* Remove duplicate errors about strict reserved bindings

* Remove duplicated error about yield/await inside params

* Don't error twice for methods in object patterns

* Don't report invalid super() twice

* Remove dup error about reserved param for expr arrows

* Remove double escapes in migrated tests

* Dedupe errors about invalid escapes in identifiers

* Remove duplicated error about decorated constructor

* Remove duplicated error about spread in flow class

* Don't throw for invalid super usage

* Don't fail for object decorators with stage 2

* Fix flow inexact type errors

* Fix flow

* Fix errors about escapes in keywords (ref: #10455)

* Update after rebase

* Fix todo

* Remove duplicated error when using += for defaults

* Remove unnecessary throw

* Nit: use ??
  • Loading branch information
nicolo-ribaudo committed Nov 5, 2019
1 parent d25262e commit 87feda7
Show file tree
Hide file tree
Showing 2,224 changed files with 155,997 additions and 3,354 deletions.
1 change: 1 addition & 0 deletions packages/babel-core/test/fixtures/parse/output.json
Expand Up @@ -2,6 +2,7 @@
"type": "File",
"start": 0,
"end": 91,
"errors": [],
"loc": {
"start": {
"line": 1,
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-parser/src/options.js
Expand Up @@ -21,6 +21,7 @@ export type Options = {
ranges: boolean,
tokens: boolean,
createParenthesizedExpressions: boolean,
errorRecovery: boolean,
};

export const defaultOptions: Options = {
Expand Down Expand Up @@ -62,6 +63,9 @@ export const defaultOptions: Options = {
// Whether to create ParenthesizedExpression AST nodes (if false
// the parser sets extra.parenthesized on the expression nodes instead).
createParenthesizedExpressions: false,
// When enabled, errors are attached to the AST instead of being directly thrown.
// Some errors will still throw, because @babel/parser can't always recover.
errorRecovery: false,
};

// Interpret and default an options object
Expand Down
138 changes: 83 additions & 55 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -108,6 +108,7 @@ export default class ExpressionParser extends LValParser {
this.unexpected();
}
expr.comments = this.state.comments;
expr.errors = this.state.errors;
return expr;
}

Expand Down Expand Up @@ -786,11 +787,11 @@ export default class ExpressionParser extends LValParser {
if (node.callee.type === "Import") {
if (node.arguments.length !== 1) {
this.raise(node.start, "import() requires exactly one argument");
}

const importArg = node.arguments[0];
if (importArg && importArg.type === "SpreadElement") {
this.raise(importArg.start, "... is not allowed in import()");
} else {
const importArg = node.arguments[0];
if (importArg && importArg.type === "SpreadElement") {
this.raise(importArg.start, "... is not allowed in import()");
}
}
}
return this.finishNode(
Expand Down Expand Up @@ -903,13 +904,6 @@ export default class ExpressionParser extends LValParser {

switch (this.state.type) {
case tt._super:
if (!this.scope.allowSuper && !this.options.allowSuperOutsideMethod) {
this.raise(
this.state.start,
"super is only allowed in object methods and classes",
);
}

node = this.startNode();
this.next();
if (
Expand All @@ -922,14 +916,26 @@ export default class ExpressionParser extends LValParser {
"super() is only valid inside a class constructor of a subclass. " +
"Maybe a typo in the method name ('constructor') or not extending another class?",
);
} else if (
!this.scope.allowSuper &&
!this.options.allowSuperOutsideMethod
) {
this.raise(
node.start,
"super is only allowed in object methods and classes",
);
}

if (
!this.match(tt.parenL) &&
!this.match(tt.bracketL) &&
!this.match(tt.dot)
) {
this.unexpected();
this.raise(
node.start,
"super can only be used with function calls (i.e. super()) or " +
"in property accesses (i.e. super.prop or super[prop])",
);
}

return this.finishNode(node, "Super");
Expand Down Expand Up @@ -1106,15 +1112,16 @@ export default class ExpressionParser extends LValParser {
}

this.next();
if (this.primaryTopicReferenceIsAllowedInCurrentTopicContext()) {
this.registerTopicReference();
return this.finishNode(node, "PipelinePrimaryTopicReference");
} else {
throw this.raise(

if (!this.primaryTopicReferenceIsAllowedInCurrentTopicContext()) {
this.raise(
node.start,
`Topic reference was used in a lexical context without topic binding`,
);
}

this.registerTopicReference();
return this.finishNode(node, "PipelinePrimaryTopicReference");
}
}

Expand Down Expand Up @@ -1199,22 +1206,22 @@ export default class ExpressionParser extends LValParser {

if (this.isContextual("meta")) {
this.expectPlugin("importMeta");

if (!this.inModule) {
this.raise(
id.start,
`import.meta may appear only with 'sourceType: "module"'`,
{ code: "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED" },
);
}
this.sawUnambiguousESM = true;
} else if (!this.hasPlugin("importMeta")) {
this.raise(
id.start,
`Dynamic imports require a parameter: import('a.js')`,
);
}

if (!this.inModule) {
this.raise(
id.start,
`import.meta may appear only with 'sourceType: "module"'`,
{ code: "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED" },
);
}
this.sawUnambiguousESM = true;

return this.parseMetaProperty(node, id, "meta");
}

Expand Down Expand Up @@ -1386,7 +1393,10 @@ export default class ExpressionParser extends LValParser {

parseNew(): N.NewExpression | N.MetaProperty {
const node = this.startNode();
const meta = this.parseIdentifier(true);

let meta = this.startNode();
this.next();
meta = this.createIdentifier(meta, "new");

if (this.eat(tt.dot)) {
const metaProp = this.parseMetaProperty(node, meta, "target");
Expand Down Expand Up @@ -1553,12 +1563,12 @@ export default class ExpressionParser extends LValParser {
this.state.start,
"Stage 2 decorators disallow object literal property decorators",
);
} else {
// we needn't check if decorators (stage 0) plugin is enabled since it's checked by
// the call to this.parseDecorator
while (this.match(tt.at)) {
decorators.push(this.parseDecorator());
}
}

// we needn't check if decorators (stage 0) plugin is enabled since it's checked by
// the call to this.parseDecorator
while (this.match(tt.at)) {
decorators.push(this.parseDecorator());
}
}

Expand Down Expand Up @@ -1933,7 +1943,7 @@ export default class ExpressionParser extends LValParser {

if (isExpression) {
node.body = this.parseMaybeAssign();
this.checkParams(node, false, allowExpression);
this.checkParams(node, false, allowExpression, false);
} else {
const nonSimple = !this.isSimpleParamList(node.params);
if (!oldStrict || nonSimple) {
Expand Down Expand Up @@ -1967,6 +1977,7 @@ export default class ExpressionParser extends LValParser {
node,
!oldStrict && !useStrict && !allowExpression && !isMethod && !nonSimple,
allowExpression,
!oldStrict && useStrict,
);
node.body = this.parseBlock(true, false);
this.state.labels = oldLabels;
Expand All @@ -1975,7 +1986,14 @@ export default class ExpressionParser extends LValParser {
this.state.inParameters = oldInParameters;
// Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval'
if (this.state.strict && node.id) {
this.checkLVal(node.id, BIND_OUTSIDE, undefined, "function name");
this.checkLVal(
node.id,
BIND_OUTSIDE,
undefined,
"function name",
undefined,
!oldStrict && useStrict,
);
}
this.state.strict = oldStrict;
}
Expand All @@ -1994,6 +2012,7 @@ export default class ExpressionParser extends LValParser {
allowDuplicates: boolean,
// eslint-disable-next-line no-unused-vars
isArrowFunction: ?boolean,
strictModeChanged?: boolean = true,
): void {
// $FlowIssue
const nameHash: {} = Object.create(null);
Expand All @@ -2003,6 +2022,8 @@ export default class ExpressionParser extends LValParser {
BIND_VAR,
allowDuplicates ? null : nameHash,
"function parameter list",
undefined,
strictModeChanged,
);
}
}
Expand Down Expand Up @@ -2084,6 +2105,8 @@ export default class ExpressionParser extends LValParser {
// Parse the next token as an identifier. If `liberal` is true (used
// when parsing properties), it will also convert keywords into
// identifiers.
// This shouldn't be used to parse the keywords of meta properties, since they
// are not identifiers and cannot contain escape sequences.

parseIdentifier(liberal?: boolean): N.Identifier {
const node = this.startNode();
Expand All @@ -2104,11 +2127,6 @@ export default class ExpressionParser extends LValParser {

if (this.match(tt.name)) {
name = this.state.value;

// An escaped identifier whose value is the same as a keyword
if (!liberal && this.state.containsEsc && isKeyword(name)) {
this.raise(this.state.pos, `Escape sequence in keyword ${name}`);
}
} else if (this.state.type.keyword) {
name = this.state.type.keyword;

Expand All @@ -2128,7 +2146,11 @@ export default class ExpressionParser extends LValParser {
throw this.unexpected();
}

if (!liberal) {
if (liberal) {
// If the current token is not used as a keyword, set its type to "tt.name".
// This will prevent this.next() from throwing about unexpected escapes.
this.state.type = tt.name;
} else {
this.checkReservedWord(
name,
this.state.start,
Expand All @@ -2153,6 +2175,7 @@ export default class ExpressionParser extends LValParser {
startLoc,
"Can not use 'yield' as identifier inside a generator",
);
return;
}

if (word === "await") {
Expand All @@ -2161,7 +2184,9 @@ export default class ExpressionParser extends LValParser {
startLoc,
"Can not use 'await' as identifier inside an async function",
);
} else if (
return;
}
if (
this.state.awaitPos === -1 &&
(this.state.maybeInArrowParameters || this.isAwaitAllowed())
) {
Expand All @@ -2174,9 +2199,11 @@ export default class ExpressionParser extends LValParser {
startLoc,
"'arguments' is not allowed in class field initializer",
);
return;
}
if (checkKeywords && isKeyword(word)) {
this.raise(startLoc, `Unexpected keyword '${word}'`);
return;
}

const reservedTest = !this.state.strict
Expand All @@ -2191,8 +2218,9 @@ export default class ExpressionParser extends LValParser {
startLoc,
"Can not use keyword 'await' outside an async function",
);
} else {
this.raise(startLoc, `Unexpected reserved word '${word}'`);
}
this.raise(startLoc, `Unexpected reserved word '${word}'`);
}
}

Expand All @@ -2206,9 +2234,6 @@ export default class ExpressionParser extends LValParser {
// Parses await expression inside async function.

parseAwait(): N.AwaitExpression {
if (this.state.awaitPos === -1) {
this.state.awaitPos = this.state.start;
}
const node = this.startNode();

this.next();
Expand All @@ -2218,8 +2243,10 @@ export default class ExpressionParser extends LValParser {
node.start,
"await is not allowed in async function parameters",
);
} else if (this.state.awaitPos === -1) {
this.state.awaitPos = node.start;
}
if (this.match(tt.star)) {
if (this.eat(tt.star)) {
this.raise(
node.start,
"await* has been removed from the async functions proposal. Use Promise.all() instead.",
Expand Down Expand Up @@ -2259,13 +2286,12 @@ export default class ExpressionParser extends LValParser {
// Parses yield expression inside generator.

parseYield(noIn?: ?boolean): N.YieldExpression {
if (this.state.yieldPos === -1) {
this.state.yieldPos = this.state.start;
}
const node = this.startNode();

if (this.state.inParameters) {
this.raise(node.start, "yield is not allowed in generator parameters");
} else if (this.state.yieldPos === -1) {
this.state.yieldPos = node.start;
}

this.next();
Expand All @@ -2291,7 +2317,7 @@ export default class ExpressionParser extends LValParser {
if (left.type === "SequenceExpression") {
// Ensure that the pipeline head is not a comma-delimited
// sequence expression.
throw this.raise(
this.raise(
leftStartPos,
`Pipeline head should not be a comma-separated sequence expression`,
);
Expand Down Expand Up @@ -2336,7 +2362,7 @@ export default class ExpressionParser extends LValParser {
pipelineStyle === "PipelineTopicExpression" &&
childExpression.type === "SequenceExpression"
) {
throw this.raise(
this.raise(
startPos,
`Pipeline body may not be a comma-separated sequence expression`,
);
Expand All @@ -2362,15 +2388,17 @@ export default class ExpressionParser extends LValParser {
break;
case "PipelineTopicExpression":
if (!this.topicReferenceWasUsedInCurrentTopicContext()) {
throw this.raise(
this.raise(
startPos,
`Pipeline is in topic style but does not use topic reference`,
);
}
bodyNode.expression = childExpression;
break;
default:
throw this.raise(startPos, `Unknown pipeline style ${pipelineStyle}`);
throw new Error(
`Internal @babel/parser error: Unknown pipeline style (${pipelineStyle})`,
);
}
return this.finishNode(bodyNode, pipelineStyle);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/babel-parser/src/parser/index.js
Expand Up @@ -39,7 +39,10 @@ export default class Parser extends StatementParser {
const file = this.startNode();
const program = this.startNode();
this.nextToken();
return this.parseTopLevel(file, program);
file.errors = null;
this.parseTopLevel(file, program);
file.errors = this.state.errors;
return file;
}
}

Expand Down

0 comments on commit 87feda7

Please sign in to comment.