diff --git a/packages/babel-parser/src/parser/error-message.js b/packages/babel-parser/src/parser/error-message.js index 7ad94f3256..90df3c5f38 100644 --- a/packages/babel-parser/src/parser/error-message.js +++ b/packages/babel-parser/src/parser/error-message.js @@ -51,7 +51,7 @@ export const ErrorMessages = Object.freeze({ ElementAfterRest: "Rest element must be last element", EscapedCharNotAnIdentifier: "Invalid Unicode escape", ExportBindingIsString: - "A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { %0 as '%1' } from 'some-module'`?", + "A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { '%0' as '%1' } from 'some-module'`?", ExportDefaultFromAsIdentifier: "'from' is not allowed as an identifier after 'export default'", ForInOfLoopInitializer: diff --git a/packages/babel-parser/src/parser/expression.js b/packages/babel-parser/src/parser/expression.js index f8b6341c70..3ed03e0e1a 100644 --- a/packages/babel-parser/src/parser/expression.js +++ b/packages/babel-parser/src/parser/expression.js @@ -94,8 +94,9 @@ export default class ExpressionParser extends LValParser { ): void { if ( prop.type === "SpreadElement" || - prop.type === "ObjectMethod" || + this.isObjectMethod(prop) || prop.computed || + // $FlowIgnore prop.shorthand ) { return; @@ -515,11 +516,7 @@ export default class ExpressionParser extends LValParser { if (arg.type === "Identifier") { this.raise(node.start, Errors.StrictDelete); - } else if ( - (arg.type === "MemberExpression" || - arg.type === "OptionalMemberExpression") && - arg.property.type === "PrivateName" - ) { + } else if (this.hasPropertyAsPrivateName(arg)) { this.raise(node.start, Errors.DeletePrivateField); } } @@ -618,12 +615,12 @@ export default class ExpressionParser extends LValParser { let optional = false; if (this.match(tt.questionDot)) { - state.optionalChainMember = optional = true; if (noCalls && this.lookaheadCharCode() === charCodes.leftParenthesis) { // stop at `?.` when parsing `new a?.()` state.stop = true; return base; } + state.optionalChainMember = optional = true; this.next(); } @@ -662,11 +659,14 @@ export default class ExpressionParser extends LValParser { ? this.parseExpression() : this.parseMaybePrivateName(true); - if (property.type === "PrivateName") { + if (this.isPrivateName(property)) { if (node.object.type === "Super") { this.raise(startPos, Errors.SuperPrivateField); } - this.classScope.usePrivateName(property.id.name, property.start); + this.classScope.usePrivateName( + this.getPrivateNameSV(property), + property.start, + ); } node.property = property; @@ -1496,13 +1496,9 @@ export default class ExpressionParser extends LValParser { // https://tc39.es/ecma262/#prod-NewExpression parseNew(node: N.Expression): N.NewExpression { node.callee = this.parseNoCallExpr(); - if (node.callee.type === "Import") { this.raise(node.callee.start, Errors.ImportCallNotNewExpression); - } else if ( - node.callee.type === "OptionalMemberExpression" || - node.callee.type === "OptionalCallExpression" - ) { + } else if (this.isOptionalChain(node.callee)) { this.raise(this.state.lastTokEnd, Errors.OptionalChainingNoNew); } else if (this.eat(tt.questionDot)) { this.raise(this.state.start, Errors.OptionalChainingNoNew); @@ -1604,7 +1600,7 @@ export default class ExpressionParser extends LValParser { if ( isRecord && - prop.type !== "ObjectProperty" && + !this.isObjectProperty(prop) && prop.type !== "SpreadElement" ) { this.raise(prop.start, Errors.InvalidRecordProperty); @@ -1923,7 +1919,7 @@ export default class ExpressionParser extends LValParser { ? this.parseExprAtom() : this.parseMaybePrivateName(isPrivateNameAllowed); - if (prop.key.type !== "PrivateName") { + if (!this.isPrivateName(prop.key)) { // ClassPrivateProperty is never computed, so we don't assign in that case. prop.computed = false; } diff --git a/packages/babel-parser/src/parser/lval.js b/packages/babel-parser/src/parser/lval.js index 14ca1fd61e..4da763b144 100644 --- a/packages/babel-parser/src/parser/lval.js +++ b/packages/babel-parser/src/parser/lval.js @@ -445,11 +445,11 @@ export default class LValParser extends NodeUtils { case "ObjectPattern": for (let prop of expr.properties) { - if (prop.type === "ObjectProperty") prop = prop.value; + if (this.isObjectProperty(prop)) 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; + else if (this.isObjectMethod(prop)) continue; this.checkLVal( prop, diff --git a/packages/babel-parser/src/parser/statement.js b/packages/babel-parser/src/parser/statement.js index 3a72687b25..986d242ee9 100644 --- a/packages/babel-parser/src/parser/statement.js +++ b/packages/babel-parser/src/parser/statement.js @@ -1346,7 +1346,7 @@ export default class StatementParser extends ExpressionParser { method.kind = "method"; this.parseClassElementName(method); - if (method.key.type === "PrivateName") { + if (this.isPrivateName(method.key)) { // Private generator method this.pushClassPrivateMethod(classBody, privateMethod, true, false); return; @@ -1370,7 +1370,7 @@ export default class StatementParser extends ExpressionParser { const containsEsc = this.state.containsEsc; const key = this.parseClassElementName(member); - const isPrivate = key.type === "PrivateName"; + const isPrivate = this.isPrivateName(key); // Check the key is not a computed expression or string literal. const isSimple = key.type === "Identifier"; const maybeQuestionTokenStart = this.state.start; @@ -1431,7 +1431,7 @@ export default class StatementParser extends ExpressionParser { this.parseClassElementName(method); this.parsePostMemberNameModifiers(publicMember); - if (method.key.type === "PrivateName") { + if (this.isPrivateName(method.key)) { // private async method this.pushClassPrivateMethod( classBody, @@ -1465,7 +1465,7 @@ export default class StatementParser extends ExpressionParser { // The so-called parsed name would have been "get/set": get the real name. this.parseClassElementName(publicMethod); - if (method.key.type === "PrivateName") { + if (this.isPrivateName(method.key)) { // private getter/setter this.pushClassPrivateMethod(classBody, privateMethod, false, false); } else { @@ -1508,7 +1508,10 @@ export default class StatementParser extends ExpressionParser { this.raise(key.start, Errors.StaticPrototype); } - if (key.type === "PrivateName" && key.id.name === "constructor") { + if ( + this.isPrivateName(key) && + this.getPrivateNameSV(key) === "constructor" + ) { this.raise(key.start, Errors.ConstructorClassPrivateField); } @@ -1571,7 +1574,7 @@ export default class StatementParser extends ExpressionParser { classBody.body.push(node); this.classScope.declarePrivateName( - node.key.id.name, + this.getPrivateNameSV(node.key), CLASS_ELEMENT_OTHER, node.key.start, ); @@ -1627,7 +1630,11 @@ export default class StatementParser extends ExpressionParser { ? CLASS_ELEMENT_STATIC_SETTER : CLASS_ELEMENT_INSTANCE_SETTER : CLASS_ELEMENT_OTHER; - this.classScope.declarePrivateName(node.key.id.name, kind, node.key.start); + this.classScope.declarePrivateName( + this.getPrivateNameSV(node.key), + kind, + node.key.start, + ); } // Overridden in typescript.js @@ -1980,7 +1987,7 @@ export default class StatementParser extends ExpressionParser { this.raise( specifier.start, Errors.ExportBindingIsString, - local.extra.raw, + local.value, exportedName, ); } else { diff --git a/packages/babel-parser/src/parser/util.js b/packages/babel-parser/src/parser/util.js index 80f0532f1f..9244cc983e 100644 --- a/packages/babel-parser/src/parser/util.js +++ b/packages/babel-parser/src/parser/util.js @@ -252,6 +252,51 @@ export default class UtilParser extends Tokenizer { this.match(tt.decimal) ); } + + /* + * Test if given node is a PrivateName + * will be overridden in ESTree plugin + */ + isPrivateName(node: Node): boolean { + return node.type === "PrivateName"; + } + + /* + * Return the string value of a given private name + * WITHOUT `#` + * @see {@link https://tc39.es/proposal-class-fields/#sec-private-names-static-semantics-stringvalue} + */ + getPrivateNameSV(node: Node): string { + return node.id.name; + } + + /* + * Return whether the given node is a member/optional chain that + * contains a private name as its property + * It is overridden in ESTree plugin + */ + hasPropertyAsPrivateName(node: Node): boolean { + return ( + (node.type === "MemberExpression" || + node.type === "OptionalMemberExpression") && + this.isPrivateName(node.property) + ); + } + + isOptionalChain(node: Node): boolean { + return ( + node.type === "OptionalMemberExpression" || + node.type === "OptionalCallExpression" + ); + } + + isObjectProperty(node: Node): boolean { + return node.type === "ObjectProperty"; + } + + isObjectMethod(node: Node): boolean { + return node.type === "ObjectMethod"; + } } /** diff --git a/packages/babel-parser/src/plugins/estree.js b/packages/babel-parser/src/plugins/estree.js index c96478e151..3368b0f138 100644 --- a/packages/babel-parser/src/plugins/estree.js +++ b/packages/babel-parser/src/plugins/estree.js @@ -5,18 +5,8 @@ import type Parser from "../parser"; import type { ExpressionErrors } from "../parser/util"; import * as N from "../types"; import type { Position } from "../util/location"; -import { type BindingTypes } from "../util/scopeflags"; import { Errors } from "../parser/error"; -function isSimpleProperty(node: N.Node): boolean { - return ( - node != null && - node.type === "Property" && - node.kind === "init" && - node.method === false - ); -} - export default (superClass: Class): Class => class extends superClass { estreeParseRegExpLiteral({ pattern, flags }: N.RegExpLiteral): N.Node { @@ -35,8 +25,13 @@ export default (superClass: Class): Class => estreeParseBigIntLiteral(value: any): N.Node { // https://github.com/estree/estree/blob/master/es2020.md#bigintliteral - // $FlowIgnore - const bigInt = typeof BigInt !== "undefined" ? BigInt(value) : null; + let bigInt; + try { + // $FlowIgnore + bigInt = BigInt(value); + } catch { + bigInt = null; + } const node = this.estreeParseLiteral(bigInt); node.bigint = String(node.value || value); @@ -98,7 +93,7 @@ export default (superClass: Class): Class => } checkDeclaration(node: N.Pattern | N.ObjectProperty): void { - if (isSimpleProperty(node)) { + if (node != null && this.isObjectProperty(node)) { this.checkDeclaration(((node: any): N.EstreeProperty).value); } else { super.checkDeclaration(node); @@ -110,44 +105,6 @@ export default (superClass: Class): Class => .params; } - checkLVal( - expr: N.Expression, - contextDescription: string, - ...args: [ - BindingTypes | void, - ?Set, - boolean | void, - boolean | void, - ] - ): void { - switch (expr.type) { - case "ObjectPattern": - expr.properties.forEach(prop => { - this.checkLVal( - prop.type === "Property" ? prop.value : prop, - "object destructuring pattern", - ...args, - ); - }); - break; - default: - super.checkLVal(expr, contextDescription, ...args); - } - } - - checkProto( - prop: N.ObjectMember | N.SpreadElement, - isRecord: boolean, - protoRef: { used: boolean }, - refExpressionErrors: ?ExpressionErrors, - ): void { - // $FlowIgnore: check prop.method and fallback to super method - if (prop.method) { - return; - } - super.checkProto(prop, isRecord, protoRef, refExpressionErrors); - } - isValidDirective(stmt: N.Statement): boolean { return ( stmt.type === "ExpressionStatement" && @@ -170,11 +127,9 @@ export default (superClass: Class): Class => parseBlockBody( node: N.BlockStatementLike, - allowDirectives: ?boolean, - topLevel: boolean, - end: TokenType, + ...args: [?boolean, boolean, TokenType, void | (boolean => void)] ): void { - super.parseBlockBody(node, allowDirectives, topLevel, end); + super.parseBlockBody(node, ...args); const directiveStatements = node.directives.map(d => this.directiveToStmt(d), @@ -337,8 +292,8 @@ export default (superClass: Class): Class => } toAssignable(node: N.Node, isLHS: boolean = false): N.Node { - if (isSimpleProperty(node)) { - this.toAssignable(node.value); + if (node != null && this.isObjectProperty(node)) { + this.toAssignable(node.value, isLHS); return node; } @@ -348,9 +303,9 @@ export default (superClass: Class): Class => toAssignableObjectExpressionProp(prop: N.Node, ...args) { if (prop.kind === "get" || prop.kind === "set") { - throw this.raise(prop.key.start, Errors.PatternHasAccessor); + this.raise(prop.key.start, Errors.PatternHasAccessor); } else if (prop.method) { - throw this.raise(prop.key.start, Errors.PatternHasMethod); + this.raise(prop.key.start, Errors.PatternHasMethod); } else { super.toAssignableObjectExpressionProp(prop, ...args); } @@ -450,4 +405,23 @@ export default (superClass: Class): Class => return node; } + + hasPropertyAsPrivateName(node: N.Node): boolean { + if (node.type === "ChainExpression") { + node = node.expression; + } + return super.hasPropertyAsPrivateName(node); + } + + isOptionalChain(node: N.Node): boolean { + return node.type === "ChainExpression"; + } + + isObjectProperty(node: N.Node): boolean { + return node.type === "Property" && node.kind === "init" && !node.method; + } + + isObjectMethod(node: N.Node): boolean { + return node.method || node.kind === "get" || node.kind === "set"; + } }; diff --git a/packages/babel-parser/test/fixtures/experimental/module-string-names/string-exported-binding-without-from/output.json b/packages/babel-parser/test/fixtures/experimental/module-string-names/string-exported-binding-without-from/output.json index ddab0a530b..5bfdc8b3f9 100644 --- a/packages/babel-parser/test/fixtures/experimental/module-string-names/string-exported-binding-without-from/output.json +++ b/packages/babel-parser/test/fixtures/experimental/module-string-names/string-exported-binding-without-from/output.json @@ -2,8 +2,8 @@ "type": "File", "start":0,"end":45,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":45}}, "errors": [ - "SyntaxError: A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { \"學而時習之,不亦說乎?\" as '學而時習之,不亦說乎?' } from 'some-module'`? (1:9)", - "SyntaxError: A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { \"吾道一以貫之。\" as '忠恕。' } from 'some-module'`? (1:24)" + "SyntaxError: A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { '學而時習之,不亦說乎?' as '學而時習之,不亦說乎?' } from 'some-module'`? (1:9)", + "SyntaxError: A string literal cannot be used as an exported binding without `from`.\n- Did you mean `export { '吾道一以貫之。' as '忠恕。' } from 'some-module'`? (1:24)" ], "program": { "type": "Program", diff --git a/packages/babel-parser/test/helpers/runFixtureTests.js b/packages/babel-parser/test/helpers/runFixtureTests.js index 4b03ee14fb..2ce9e888d5 100644 --- a/packages/babel-parser/test/helpers/runFixtureTests.js +++ b/packages/babel-parser/test/helpers/runFixtureTests.js @@ -83,7 +83,13 @@ export function runThrowTestsWithEstree(fixturesPath, parseFunction) { Object.keys(fixtures).forEach(function (name) { fixtures[name].forEach(function (testSuite) { testSuite.tests.forEach(function (task) { - if (!task.options.throws) return; + if (!task.options.throws) { + const hasErrors = + !task.disabled && "errors" in JSON.parse(task.expect.code); + if (!hasErrors) { + return; + } + } task.options.plugins = task.options.plugins || []; task.options.plugins.push("estree"); @@ -92,7 +98,7 @@ export function runThrowTestsWithEstree(fixturesPath, parseFunction) { testFn(name + "/" + testSuite.title + "/" + task.title, function () { try { - runTest(task, parseFunction); + runTest(task, parseFunction, true); } catch (err) { const fixturePath = `${path.relative( rootPath, @@ -126,7 +132,19 @@ function save(test, ast) { ); } -function runTest(test, parseFunction) { +/** + * run parser on given tests + * + * @param {Test} A {@link packages/babel-helper-fixtures/src/index.js Test} instance + generated from `getFixtures` + * @param {*} parseFunction A parser with the same interface of `@babel/parser#parse` + * @param {boolean} [compareErrorsOnly=false] Whether we should only compare the "errors" + * of generated ast against the expected AST. Used for `runThrowTestsWithEstree` where an + * ESTree AST is generated but we want to make sure `@babel/parser` still throws expected + * recoverable errors on given code locations. + * @returns {void} + */ +function runTest(test, parseFunction, compareErrorsOnly = false) { const opts = test.options; if (opts.throws && test.expect.code) { @@ -189,6 +207,11 @@ function runTest(test, parseFunction) { throw new Error( "Expected error message: " + opts.throws + ". But parsing succeeded.", ); + } else if (compareErrorsOnly) { + const mis = misMatch(JSON.parse(test.expect.code).errors, ast.errors); + if (mis) { + throw new Error(mis); + } } else { const mis = misMatch(JSON.parse(test.expect.code), ast);