@babel/parser error recovery (#10363)

* 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 ??
This commit is contained in:
Nicolò Ribaudo
2019-11-05 10:15:00 +01:00
committed by GitHub
parent d25262ec4b
commit 87feda7c2a
2224 changed files with 155996 additions and 3353 deletions

View File

@@ -108,6 +108,7 @@ export default class ExpressionParser extends LValParser {
this.unexpected();
}
expr.comments = this.state.comments;
expr.errors = this.state.errors;
return expr;
}
@@ -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(
@@ -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 (
@@ -922,6 +916,14 @@ 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 (
@@ -929,7 +931,11 @@ export default class ExpressionParser extends LValParser {
!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");
@@ -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");
}
}
@@ -1199,6 +1206,15 @@ 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,
@@ -1206,15 +1222,6 @@ export default class ExpressionParser extends LValParser {
);
}
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");
}
@@ -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");
@@ -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());
}
}
@@ -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) {
@@ -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;
@@ -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;
}
@@ -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);
@@ -2003,6 +2022,8 @@ export default class ExpressionParser extends LValParser {
BIND_VAR,
allowDuplicates ? null : nameHash,
"function parameter list",
undefined,
strictModeChanged,
);
}
}
@@ -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();
@@ -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;
@@ -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,
@@ -2153,6 +2175,7 @@ export default class ExpressionParser extends LValParser {
startLoc,
"Can not use 'yield' as identifier inside a generator",
);
return;
}
if (word === "await") {
@@ -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())
) {
@@ -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
@@ -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}'`);
}
}
@@ -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();
@@ -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.",
@@ -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();
@@ -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`,
);
@@ -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`,
);
@@ -2362,7 +2388,7 @@ 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`,
);
@@ -2370,7 +2396,9 @@ export default class ExpressionParser extends LValParser {
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);
}

View File

@@ -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;
}
}

View File

@@ -10,6 +10,8 @@ import CommentsParser from "./comments";
// message.
export default class LocationParser extends CommentsParser {
+isLookahead: boolean;
getLocationForPosition(pos: number): Position {
let loc;
if (pos === this.state.start) loc = this.state.startLoc;
@@ -31,7 +33,7 @@ export default class LocationParser extends CommentsParser {
missingPluginNames?: Array<string>,
code?: string,
} = {},
): empty {
): Error | empty {
const loc = this.getLocationForPosition(pos);
message += ` (${loc.line}:${loc.column})`;
@@ -47,6 +49,12 @@ export default class LocationParser extends CommentsParser {
if (code !== undefined) {
err.code = code;
}
throw err;
if (this.options.errorRecovery) {
if (!this.isLookahead) this.state.errors.push(err);
return err;
} else {
throw err;
}
}
}

View File

@@ -15,7 +15,10 @@ import type {
SpreadElement,
} from "../types";
import type { Pos, Position } from "../util/location";
import { isStrictBindReservedWord } from "../util/identifier";
import {
isStrictBindOnlyReservedWord,
isStrictBindReservedWord,
} from "../util/identifier";
import { NodeUtils } from "./node";
import { type BindingTypes, BIND_NONE } from "../util/scopeflags";
@@ -37,6 +40,8 @@ export default class LValParser extends NodeUtils {
// Convert existing expression atom to assignable pattern
// if possible.
// NOTE: There is a corresponding "isAssignable" method in flow.js.
// When this one is updated, please check if also that one needs to be updated.
toAssignable(
node: Node,
@@ -96,15 +101,16 @@ export default class LValParser extends NodeUtils {
break;
case "AssignmentExpression":
if (node.operator === "=") {
node.type = "AssignmentPattern";
delete node.operator;
} else {
if (node.operator !== "=") {
this.raise(
node.left.end,
"Only '=' operator can be used for specifying default value.",
);
}
node.type = "AssignmentPattern";
delete node.operator;
this.toAssignable(node.left, isBinding, contextDescription);
break;
case "ParenthesizedExpression":
@@ -118,14 +124,9 @@ export default class LValParser extends NodeUtils {
case "MemberExpression":
if (!isBinding) break;
default: {
const message =
"Invalid left-hand side" +
(contextDescription
? " in " + contextDescription
: /* istanbul ignore next */ "expression");
this.raise(node.start, message);
}
default:
// We don't know how to deal with this node. It will
// be reported by a later call to checkLVal
}
}
return node;
@@ -349,12 +350,18 @@ export default class LValParser extends NodeUtils {
checkClashes: ?{ [key: string]: boolean },
contextDescription: string,
disallowLetBinding?: boolean,
strictModeChanged?: boolean = false,
): void {
switch (expr.type) {
case "Identifier":
if (
this.state.strict &&
isStrictBindReservedWord(expr.name, this.inModule)
// "Global" reserved words have already been checked by parseIdentifier,
// unless they have been found in the id or parameters of a strict-mode
// function in a sloppy context.
(strictModeChanged
? isStrictBindReservedWord(expr.name, this.inModule)
: isStrictBindOnlyReservedWord(expr.name))
) {
this.raise(
expr.start,
@@ -404,6 +411,11 @@ export default class LValParser extends NodeUtils {
case "ObjectPattern":
for (let prop of expr.properties) {
if (prop.type === "ObjectProperty") prop = prop.value;
// If we find here an ObjectMethod, it's because this was originally
// an ObjectExpression which has then been converted.
// toAssignable already reported this error with a nicer message.
else if (prop.type === "ObjectMethod") continue;
this.checkLVal(
prop,
bindingType,
@@ -489,7 +501,7 @@ export default class LValParser extends NodeUtils {
}
raiseRestNotLast(pos: number) {
this.raise(pos, `Rest element must be last element`);
throw this.raise(pos, `Rest element must be last element`);
}
raiseTrailingCommaAfterRest(pos: number) {

View File

@@ -203,7 +203,7 @@ export default class StatementParser extends ExpressionParser {
case tt._var:
kind = kind || this.state.value;
if (context && kind !== "var") {
this.unexpected(
this.raise(
this.state.start,
"Lexical declaration cannot appear in a single-statement context",
);
@@ -269,8 +269,8 @@ export default class StatementParser extends ExpressionParser {
default: {
if (this.isAsyncFunction()) {
if (context) {
this.unexpected(
null,
this.raise(
this.state.start,
"Async functions can only be declared at the top level or inside a block",
);
}
@@ -351,7 +351,7 @@ export default class StatementParser extends ExpressionParser {
);
}
} else if (!this.canHaveLeadingDecorator()) {
this.raise(
throw this.raise(
this.state.start,
"Leading decorators must be attached to a class declaration",
);
@@ -1036,7 +1036,7 @@ export default class StatementParser extends ExpressionParser {
this.initFunction(node, isAsync);
if (this.match(tt.star) && isHangingStatement) {
this.unexpected(
this.raise(
this.state.start,
"Generators can only be declared at the top level or inside a block",
);
@@ -1077,10 +1077,10 @@ export default class StatementParser extends ExpressionParser {
this.scope.exit();
if (isStatement && !isHangingStatement) {
// We need to validate this _after_ parsing the function body
// We need to register this _after_ parsing the function body
// because of TypeScript body-less function declarations,
// which shouldn't be added to the scope.
this.checkFunctionStatementId(node);
this.registerFunctionStatementId(node);
}
this.state.maybeInArrowParameters = oldMaybeInArrowParameters;
@@ -1111,22 +1111,21 @@ export default class StatementParser extends ExpressionParser {
this.checkYieldAwaitInDefaultParams();
}
checkFunctionStatementId(node: N.Function): void {
registerFunctionStatementId(node: N.Function): void {
if (!node.id) return;
// If it is a regular function declaration in sloppy mode, then it is
// subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding
// mode depends on properties of the current scope (see
// treatFunctionsAsVar).
this.checkLVal(
node.id,
this.scope.declareName(
node.id.name,
this.state.strict || node.generator || node.async
? this.scope.treatFunctionsAsVar
? BIND_VAR
: BIND_LEXICAL
: BIND_FUNCTION,
null,
"function name",
node.id.start,
);
}
@@ -1191,7 +1190,7 @@ export default class StatementParser extends ExpressionParser {
while (!this.eat(tt.braceR)) {
if (this.eat(tt.semi)) {
if (decorators.length > 0) {
this.raise(
throw this.raise(
this.state.lastTokEnd,
"Decorators must not be followed by a semicolon",
);
@@ -1229,7 +1228,7 @@ export default class StatementParser extends ExpressionParser {
});
if (decorators.length) {
this.raise(
throw this.raise(
this.state.start,
"You have trailing decorators with no method",
);
@@ -1362,13 +1361,6 @@ export default class StatementParser extends ExpressionParser {
if (isConstructor) {
publicMethod.kind = "constructor";
if (publicMethod.decorators) {
this.raise(
publicMethod.start,
"You can't attach decorators to a class constructor",
);
}
// TypeScript allows multiple overloaded constructor declarations.
if (state.hadConstructor && !this.hasPlugin("typescript")) {
this.raise(key.start, "Duplicate constructor in the same class");
@@ -1797,7 +1789,7 @@ export default class StatementParser extends ExpressionParser {
this.hasPlugin("decorators") &&
this.getPluginOption("decorators", "decoratorsBeforeExport")
) {
this.unexpected(
this.raise(
this.state.start,
"Decorators must be placed *before* the 'export' keyword." +
" You can set the 'decoratorsBeforeExport' option to false to use" +
@@ -1807,7 +1799,7 @@ export default class StatementParser extends ExpressionParser {
this.parseDecorators(false);
return this.parseClass(expr, true, true);
} else if (this.match(tt._const) || this.match(tt._var) || this.isLet()) {
return this.raise(
throw this.raise(
this.state.start,
"Only expressions, functions or classes are allowed as the `default` export.",
);
@@ -1977,7 +1969,7 @@ export default class StatementParser extends ExpressionParser {
name: string,
): void {
if (this.state.exportedIdentifiers.indexOf(name) > -1) {
throw this.raise(
this.raise(
node.start,
name === "default"
? "Only one default export allowed per module."
@@ -2098,8 +2090,8 @@ export default class StatementParser extends ExpressionParser {
} else {
// Detect an attempt to deep destructure
if (this.eat(tt.colon)) {
this.unexpected(
null,
throw this.raise(
this.state.start,
"ES2015 named imports do not destructure. " +
"Use another statement for destructuring after the import.",
);

View File

@@ -2,6 +2,7 @@
import { types as tt, type TokenType } from "../tokenizer/types";
import Tokenizer from "../tokenizer";
import State from "../tokenizer/state";
import type { Node } from "../types";
import { lineBreak, skipWhiteSpace } from "../util/whitespace";
import { isIdentifierChar } from "../util/identifier";
@@ -9,6 +10,14 @@ import * as charCodes from "charcodes";
const literal = /^('|")((?:\\?.)*?)\1/;
type TryParse<Node, Error, Thrown, Aborted, FailState> = {
node: Node,
error: Error,
thrown: Thrown,
aborted: Aborted,
failState: FailState,
};
// ## Parser utilities
export default class UtilParser extends Tokenizer {
@@ -215,4 +224,58 @@ export default class UtilParser extends Tokenizer {
return false;
}
// tryParse will clone parser state.
// It is expensive and should be used with cautions
tryParse<T: Node | $ReadOnlyArray<Node>>(
fn: (abort: (node?: T) => empty) => T,
oldState: State = this.state.clone(),
):
| TryParse<T, null, false, false, null>
| TryParse<T | null, SyntaxError, boolean, false, State>
| TryParse<T | null, null, false, true, State> {
const abortSignal: { node: T | null } = { node: null };
try {
const node = fn((node = null) => {
abortSignal.node = node;
throw abortSignal;
});
if (this.state.errors.length > oldState.errors.length) {
const failState = this.state;
this.state = oldState;
return {
node,
error: (failState.errors[oldState.errors.length]: SyntaxError),
thrown: false,
aborted: false,
failState,
};
}
return {
node,
error: null,
thrown: false,
aborted: false,
failState: null,
};
} catch (error) {
const failState = this.state;
this.state = oldState;
if (error instanceof SyntaxError) {
return { node: null, error, thrown: true, aborted: false, failState };
}
if (error === abortSignal) {
return {
node: abortSignal.node,
error: null,
thrown: false,
aborted: true,
failState,
};
}
throw error;
}
}
}