Skip to content

Commit

Permalink
Refactor: add parser message template (#11192)
Browse files Browse the repository at this point in the history
* refactor: add raiseWithData method

* refactor: error message template

* fix missing plugin error structure

* fix flow errors

* refactor: use error message template in eslint plugin

* refacotr: use error message template in flow plugin

* refactor: use error message template in typescript plugin

* refactor: use error message template in jsx plugin

* address review comments

* Update packages/babel-parser/src/parser/location.js

Co-Authored-By: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
  • Loading branch information
JLHwung and nicolo-ribaudo committed Mar 3, 2020
1 parent 114f672 commit 21c9141
Show file tree
Hide file tree
Showing 12 changed files with 618 additions and 564 deletions.
177 changes: 50 additions & 127 deletions packages/babel-parser/src/parser/expression.js

Large diffs are not rendered by default.

223 changes: 201 additions & 22 deletions packages/babel-parser/src/parser/location.js
@@ -1,5 +1,5 @@
// @flow

/* eslint sort-keys: "error" */
import { getLineInfo, type Position } from "../util/location";
import CommentsParser from "./comments";

Expand All @@ -9,6 +9,188 @@ import CommentsParser from "./comments";
// of the error message, and then raises a `SyntaxError` with that
// message.

type ErrorContext = {
pos: number,
loc: Position,
missingPlugin?: Array<string>,
code?: string,
};

// The Errors key follows https://cs.chromium.org/chromium/src/v8/src/common/message-template.h unless it does not exist
export const Errors = Object.freeze({
ArgumentsDisallowedInInitializer:
"'arguments' is not allowed in class field initializer",
AsyncFunctionInSingleStatementContext:
"Async functions can only be declared at the top level or inside a block",
AwaitBindingIdentifier:
"Can not use 'await' as identifier inside an async function",
AwaitExpressionFormalParameter:
"await is not allowed in async function parameters",
AwaitNotInAsyncFunction:
"Can not use keyword 'await' outside an async function",
BadGetterArity: "getter must not have any formal parameters",
BadSetterArity: "setter must have exactly one formal parameter",
BadSetterRestParameter:
"setter function argument must not be a rest parameter",
ConstructorClassField: "Classes may not have a field named 'constructor'",
ConstructorClassPrivateField:
"Classes may not have a private field named '#constructor'",
// todo: rephrase to get/set accessor
ConstructorIsAccessor: "Constructor can't have get/set modifier",
ConstructorIsAsync: "Constructor can't be an async function",
ConstructorIsGenerator: "Constructor can't be a generator",
DeclarationMissingInitializer: "%0 require an initialization value",
DecoratorBeforeExport:
"Decorators must be placed *before* the 'export' keyword. You can set the 'decoratorsBeforeExport' option to false to use the 'export @decorator class {}' syntax",
DecoratorConstructor:
"Decorators can't be used with a constructor. Did you mean '@dec class { ... }'?",
DecoratorExportClass:
"Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead.",
DecoratorSemicolon: "Decorators must not be followed by a semicolon",
DeletePrivateField: "Deleting a private field is not allowed",
DestructureNamedImport:
"ES2015 named imports do not destructure. Use another statement for destructuring after the import.",
DuplicateConstructor: "Duplicate constructor in the same class",
DuplicateDefaultExport: "Only one default export allowed per module.",
DuplicateExport:
"`%0` has already been exported. Exported identifiers must be unique.",
DuplicateProto: "Redefinition of __proto__ property",
DuplicateRegExpFlags: "Duplicate regular expression flag",
ElementAfterRest: "Rest element must be last element",
EscapedCharNotAnIdentifier: "Invalid Unicode escape",
ForInOfLoopInitializer:
"%0 loop variable declaration may not have an initializer",
GeneratorInSingleStatementContext:
"Generators can only be declared at the top level or inside a block",
IllegalBreakContinue: "Unsyntactic %0",
IllegalLanguageModeDirective:
"Illegal 'use strict' directive in function with non-simple parameter list",
IllegalReturn: "'return' outside of function",
ImportCallArgumentTrailingComma:
"Trailing comma is disallowed inside import(...) arguments",
ImportCallArity: "import() requires exactly one argument",
ImportCallArityLtOne: "Dynamic imports require a parameter: import('a.js')",
ImportCallNotNewExpression: "Cannot use new with import(...)",
ImportCallSpreadArgument: "... is not allowed in import()",
ImportMetaOutsideModule: `import.meta may appear only with 'sourceType: "module"'`,
ImportOutsideModule: `'import' and 'export' may appear only with 'sourceType: "module"'`,
InvalidCodePoint: "Code point out of bounds",
InvalidDigit: "Expected number in radix %0",
InvalidEscapeSequence: "Bad character escape sequence",
InvalidEscapeSequenceTemplate: "Invalid escape sequence in template",
InvalidEscapedReservedWord: "Escape sequence in keyword %0",
InvalidIdentifier: "Invalid identifier %0",
InvalidLhs: "Invalid left-hand side in %0",
InvalidLhsBinding: "Binding invalid left-hand side in %0",
InvalidNumber: "Invalid number",
InvalidOrUnexpectedToken: "Unexpected character '%0'",
InvalidParenthesizedAssignment: "Invalid parenthesized assignment pattern",
InvalidPrivateFieldResolution: "Private name #%0 is not defined",
InvalidPropertyBindingPattern: "Binding member expression",
InvalidRestAssignmentPattern: "Invalid rest operator's argument",
LabelRedeclaration: "Label '%0' is already declared",
LetInLexicalBinding:
"'let' is not allowed to be used as a name in 'let' or 'const' declarations.",
MalformedRegExpFlags: "Invalid regular expression flag",
MissingClassName: "A class name is required",
MissingEqInAssignment:
"Only '=' operator can be used for specifying default value.",
MissingUnicodeEscape: "Expecting Unicode escape sequence \\uXXXX",
MixingCoalesceWithLogical:
"Nullish coalescing operator(??) requires parens when mixing with logical operators",
ModuleExportUndefined: "Export '%0' is not defined",
MultipleDefaultsInSwitch: "Multiple default clauses",
NewlineAfterThrow: "Illegal newline after throw",
NoCatchOrFinally: "Missing catch or finally clause",
NumberIdentifier: "Identifier directly after number",
NumericSeparatorInEscapeSequence:
"Numeric separators are not allowed inside unicode escape sequences or hex escape sequences",
ObsoleteAwaitStar:
"await* has been removed from the async functions proposal. Use Promise.all() instead.",
OptionalChainingNoNew:
"constructors in/after an Optional Chain are not allowed",
OptionalChainingNoTemplate:
"Tagged Template Literals are not allowed in optionalChain",
ParamDupe: "Argument name clash",
PatternHasAccessor: "Object pattern can't contain getter or setter",
PatternHasMethod: "Object pattern can't contain methods",
PipelineBodyNoArrow:
'Unexpected arrow "=>" after pipeline body; arrow function in pipeline body must be parenthesized',
PipelineBodySequenceExpression:
"Pipeline body may not be a comma-separated sequence expression",
PipelineHeadSequenceExpression:
"Pipeline head should not be a comma-separated sequence expression",
PipelineTopicUnused:
"Pipeline is in topic style but does not use topic reference",
PrimaryTopicNotAllowed:
"Topic reference was used in a lexical context without topic binding",
PrimaryTopicRequiresSmartPipeline:
"Primary Topic Reference found but pipelineOperator not passed 'smart' for 'proposal' option.",
PrivateNameRedeclaration: "Duplicate private name #%0",
RestTrailingComma: "Unexpected trailing comma after rest element",
SloppyFunction:
"In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement",
StaticPrototype: "Classes may not have static property named prototype",
StrictDelete: "Deleting local variable in strict mode",
StrictEvalArguments: "Assigning to '%0' in strict mode",
StrictEvalArgumentsBinding: "Binding '%0' in strict mode",
StrictFunction:
"In strict mode code, functions can only be declared at top level or inside a block",
StrictOctalLiteral: "Octal literal in strict mode",
StrictWith: "'with' in strict mode",
SuperNotAllowed:
"super() is only valid inside a class constructor of a subclass. Maybe a typo in the method name ('constructor') or not extending another class?",
SuperPrivateField: "Private fields can't be accessed on super",
//todo: rephrase this error message as it is too subjective
TrailingDecorator: "You have trailing decorators with no method",
UnexpectedArgumentPlaceholder: "Unexpected argument placeholder",
UnexpectedAwaitAfterPipelineBody:
'Unexpected "await" after pipeline body; await must have parentheses in minimal proposal',
UnexpectedDigitAfterHash: "Unexpected digit after hash token",
UnexpectedImportExport:
"'import' and 'export' may only appear at the top level",
UnexpectedKeyword: "Unexpected keyword '%0'",
UnexpectedLeadingDecorator:
"Leading decorators must be attached to a class declaration",
UnexpectedLexicalDeclaration:
"Lexical declaration cannot appear in a single-statement context",
UnexpectedNewTarget: "new.target can only be used in functions",
UnexpectedNumericSeparator:
"A numeric separator is only allowed between two digits",
UnexpectedPrivateField:
"Private names can only be used as the name of a class element (i.e. class C { #p = 42; #m() {} } )\n or a property of member expression (i.e. this.#p).",
UnexpectedReservedWord: "Unexpected reserved word '%0'",
UnexpectedSuper: "super is only allowed in object methods and classes",
UnexpectedToken: "Unexpected token '%'",
UnexpectedTokenUnaryExponentiation:
"Illegal expression. Wrap left hand side or entire exponentiation in parentheses.",
UnsupportedBind: "Binding should be performed on object property.",
//todo: rephrase this error message as it is too subjective
UnsupportedDecoratorExport:
"You can only use decorators on an export when exporting a class",
UnsupportedDefaultExport:
"Only expressions, functions or classes are allowed as the `default` export.",
UnsupportedImport: "import can only be used in import() or import.meta",
UnsupportedMetaProperty: "The only valid meta property for %0 is %0.%1",
//todo: remove Stage 2 as we are likely to forget updating when it progressed
UnsupportedParameterDecorator:
"Stage 2 decorators cannot be used to decorate parameters",
UnsupportedPropertyDecorator:
"Stage 2 decorators disallow object literal property decorators",
UnsupportedSuper:
"super can only be used with function calls (i.e. super()) or in property accesses (i.e. super.prop or super[prop])",
UnterminatedComment: "Unterminated comment",
UnterminatedRegExp: "Unterminated regular expression",
UnterminatedString: "Unterminated string constant",
UnterminatedTemplate: "Unterminated template",
VarRedeclaration: "Identifier '%0' has already been declared",
YieldBindingIdentifier:
"Can not use 'yield' as identifier inside a generator",
YieldInParameter: "yield is not allowed in generator parameters",
ZeroDigitNumericSeparator:
"Numeric separator can not be used after leading 0",
});

export default class LocationParser extends CommentsParser {
// Forward-declaration: defined in tokenizer/index.js
/*::
Expand All @@ -26,33 +208,30 @@ export default class LocationParser extends CommentsParser {
return loc;
}

raise(
raise(pos: number, errorTemplate: string, ...params: any): Error | empty {
return this.raiseWithData(pos, undefined, errorTemplate, ...params);
}

raiseWithData(
pos: number,
message: string,
{
missingPluginNames,
code,
}: {
missingPluginNames?: Array<string>,
data?: {
missingPlugin?: Array<string>,
code?: string,
} = {},
},
errorTemplate: string,
...params: any
): Error | empty {
const loc = this.getLocationForPosition(pos);
const message =
errorTemplate.replace(/%(\d+)/g, (_, i: number) => params[i]) +
` (${loc.line}:${loc.column})`;
return this._raise(Object.assign(({ loc, pos }: Object), data), message);
}

message += ` (${loc.line}:${loc.column})`;
_raise(errorContext: ErrorContext, message: string): Error | empty {
// $FlowIgnore
const err: SyntaxError & { pos: number, loc: Position } = new SyntaxError(
message,
);
err.pos = pos;
err.loc = loc;
if (missingPluginNames) {
err.missingPlugin = missingPluginNames;
}
if (code !== undefined) {
err.code = code;
}

const err: SyntaxError & ErrorContext = new SyntaxError(message);
Object.assign(err, errorContext);
if (this.options.errorRecovery) {
if (!this.isLookahead) this.state.errors.push(err);
return err;
Expand Down
66 changes: 34 additions & 32 deletions packages/babel-parser/src/parser/lval.js
Expand Up @@ -22,6 +22,7 @@ import {
import { NodeUtils } from "./node";
import { type BindingTypes, BIND_NONE } from "../util/scopeflags";
import { ExpressionErrors } from "./util";
import { Errors } from "./location";

const unwrapParenthesizedExpression = (node: Node) => {
return node.type === "ParenthesizedExpression"
Expand Down Expand Up @@ -62,7 +63,7 @@ export default class LValParser extends NodeUtils {
parenthesized.type !== "Identifier" &&
parenthesized.type !== "MemberExpression"
) {
this.raise(node.start, "Invalid parenthesized assignment pattern");
this.raise(node.start, Errors.InvalidParenthesizedAssignment);
}
}

Expand Down Expand Up @@ -114,10 +115,7 @@ export default class LValParser extends NodeUtils {

case "AssignmentExpression":
if (node.operator !== "=") {
this.raise(
node.left.end,
"Only '=' operator can be used for specifying default value.",
);
this.raise(node.left.end, Errors.MissingEqInAssignment);
}

node.type = "AssignmentPattern";
Expand All @@ -140,8 +138,8 @@ export default class LValParser extends NodeUtils {
if (prop.type === "ObjectMethod") {
const error =
prop.kind === "get" || prop.kind === "set"
? "Object pattern can't contain getter or setter"
: "Object pattern can't contain methods";
? Errors.PatternHasAccessor
: Errors.PatternHasMethod;

this.raise(prop.key.start, error);
} else if (prop.type === "SpreadElement" && !isLast) {
Expand Down Expand Up @@ -288,10 +286,7 @@ export default class LValParser extends NodeUtils {
} else {
const decorators = [];
if (this.match(tt.at) && this.hasPlugin("decorators")) {
this.raise(
this.state.start,
"Stage 2 decorators cannot be used to decorate parameters",
);
this.raise(this.state.start, Errors.UnsupportedParameterDecorator);
}
while (this.match(tt.at)) {
decorators.push(this.parseDecorator());
Expand Down Expand Up @@ -361,9 +356,10 @@ export default class LValParser extends NodeUtils {
) {
this.raise(
expr.start,
`${bindingType === BIND_NONE ? "Assigning to" : "Binding"} '${
expr.name
}' in strict mode`,
bindingType === BIND_NONE
? Errors.StrictEvalArguments
: Errors.StrictEvalArgumentsBinding,
expr.name,
);
}

Expand All @@ -382,16 +378,13 @@ export default class LValParser extends NodeUtils {
const key = `_${expr.name}`;

if (checkClashes[key]) {
this.raise(expr.start, "Argument name clash");
this.raise(expr.start, Errors.ParamDupe);
} else {
checkClashes[key] = true;
}
}
if (disallowLetBinding && expr.name === "let") {
this.raise(
expr.start,
"'let' is not allowed to be used as a name in 'let' or 'const' declarations.",
);
this.raise(expr.start, Errors.LetInLexicalBinding);
}
if (!(bindingType & BIND_NONE)) {
this.scope.declareName(expr.name, bindingType, expr.start);
Expand All @@ -400,7 +393,7 @@ export default class LValParser extends NodeUtils {

case "MemberExpression":
if (bindingType !== BIND_NONE) {
this.raise(expr.start, "Binding member expression");
this.raise(expr.start, Errors.InvalidPropertyBindingPattern);
}
break;

Expand Down Expand Up @@ -464,15 +457,24 @@ export default class LValParser extends NodeUtils {
break;

default: {
const message =
(bindingType === BIND_NONE
? "Invalid"
: /* istanbul ignore next */ "Binding invalid") +
" left-hand side" +
(contextDescription
? " in " + contextDescription
: /* istanbul ignore next */ "expression");
this.raise(expr.start, message);
if (contextDescription) {
this.raise(
expr.start,
bindingType === BIND_NONE
? Errors.InvalidLhs
: Errors.InvalidLhsBinding,
contextDescription,
);
} else {
// todo: check if contextDescription is never empty
const message =
(bindingType === BIND_NONE
? "Invalid"
: /* istanbul ignore next */ "Binding invalid") +
" left-hand side expression";

this.raise(expr.start, message);
}
}
}
}
Expand All @@ -482,7 +484,7 @@ export default class LValParser extends NodeUtils {
node.argument.type !== "Identifier" &&
node.argument.type !== "MemberExpression"
) {
this.raise(node.argument.start, "Invalid rest operator's argument");
this.raise(node.argument.start, Errors.InvalidRestAssignmentPattern);
}
}

Expand All @@ -497,10 +499,10 @@ export default class LValParser extends NodeUtils {
}

raiseRestNotLast(pos: number) {
throw this.raise(pos, `Rest element must be last element`);
throw this.raise(pos, Errors.ElementAfterRest);
}

raiseTrailingCommaAfterRest(pos: number) {
this.raise(pos, `Unexpected trailing comma after rest element`);
this.raise(pos, Errors.RestTrailingComma);
}
}

0 comments on commit 21c9141

Please sign in to comment.