diff --git a/js/src/frontend/ParseNode.h b/js/src/frontend/ParseNode.h index 498be8ac456..f611588352e 100644 --- a/js/src/frontend/ParseNode.h +++ b/js/src/frontend/ParseNode.h @@ -332,6 +332,8 @@ enum ParseNodeKind * in original source, not introduced via * constant folding or other tree rewriting * PNK_LABEL name pn_atom: label, pn_expr: labeled statement + * PNK_IMPORT binary pn_left: PNK_IMPORT_SPEC_LIST import specifiers + * pn_right: PNK_STRING module specifier * * * All left-associated binary trees of the same type are optimized into lists diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index 1b342a6d7a8..acc44768b5a 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -4177,6 +4177,105 @@ Parser::letDeclarationOrBlock(YieldHandling yieldHandling) return SyntaxParseHandler::NodeFailure; } +template<> +bool +Parser::namedImportsOrNamespaceImport(TokenKind tt, Node importSpecSet) +{ + if (tt == TOK_LC) { + while (true) { + // Handle the forms |import {} from 'a'| and + // |import { ..., } from 'a'| (where ... is non empty), by + // escaping the loop early if the next token is }. + if (!tokenStream.peekToken(&tt, TokenStream::KeywordIsName)) + return false; + + if (tt == TOK_RC) + break; + + // If the next token is a keyword, the previous call to + // peekToken matched it as a TOK_NAME, and put it in the + // lookahead buffer, so this call will match keywords as well. + MUST_MATCH_TOKEN(TOK_NAME, JSMSG_NO_IMPORT_NAME); + Node importName = newName(tokenStream.currentName()); + if (!importName) + return false; + + if (!tokenStream.getToken(&tt)) + return false; + + if (tt == TOK_NAME && tokenStream.currentName() == context->names().as) { + MUST_MATCH_TOKEN(TOK_NAME, JSMSG_NO_BINDING_NAME); + } else { + // Keywords cannot be bound to themselves, so an import name + // that is a keyword is a syntax error if it is not followed + // by the keyword 'as'. + // See the ImportSpecifier production in ES6 section 15.2.2. + if (IsKeyword(importName->name())) { + JSAutoByteString bytes; + if (!AtomToPrintableString(context, importName->name(), &bytes)) + return false; + report(ParseError, false, null(), JSMSG_AS_AFTER_RESERVED_WORD, bytes.ptr()); + return false; + } + tokenStream.ungetToken(); + } + Node bindingName = newName(tokenStream.currentName()); + if (!bindingName) + return false; + + Node importSpec = handler.newBinary(PNK_IMPORT_SPEC, importName, bindingName); + if (!importSpec) + return false; + + handler.addList(importSpecSet, importSpec); + + bool matched; + if (!tokenStream.matchToken(&matched, TOK_COMMA)) + return false; + + if (!matched) + break; + } + + MUST_MATCH_TOKEN(TOK_RC, JSMSG_RC_AFTER_IMPORT_SPEC_LIST); + } else { + MOZ_ASSERT(tt == TOK_MUL); + if (!tokenStream.getToken(&tt)) + return false; + + if (tt != TOK_NAME || tokenStream.currentName() != context->names().as) { + report(ParseError, false, null(), JSMSG_AS_AFTER_IMPORT_STAR); + return false; + } + + MUST_MATCH_TOKEN(TOK_NAME, JSMSG_NO_BINDING_NAME); + + Node importName = newName(context->names().star); + if (!importName) + return null(); + + Node bindingName = newName(tokenStream.currentName()); + if (!bindingName) + return false; + + Node importSpec = handler.newBinary(PNK_IMPORT_SPEC, importName, bindingName); + if (!importSpec) + return false; + + handler.addList(importSpecSet, importSpec); + } + + return true; +} + +template<> +bool +Parser::namedImportsOrNamespaceImport(TokenKind tt, Node importSpecSet) +{ + MOZ_ALWAYS_FALSE(abortIfSyntaxParser()); + return false; +} + template typename ParseHandler::Node Parser::importDeclaration() @@ -4197,7 +4296,7 @@ Parser::importDeclaration() if (!importSpecSet) return null(); - if (tt == TOK_NAME || tt == TOK_LC) { + if (tt == TOK_NAME || tt == TOK_LC || tt == TOK_MUL) { if (tt == TOK_NAME) { // Handle the form |import a from 'b'|, by adding a single import // specifier to the list, with 'default' as the import name and @@ -4216,83 +4315,43 @@ Parser::importDeclaration() return null(); handler.addList(importSpecSet, importSpec); - } else { - while (true) { - // Handle the forms |import {} from 'a'| and - // |import { ..., } from 'a'| (where ... is non empty), by - // escaping the loop early if the next token is }. - if (!tokenStream.peekToken(&tt, TokenStream::KeywordIsName)) - return null(); - if (tt == TOK_RC) - break; - // If the next token is a keyword, the previous call to - // peekToken matched it as a TOK_NAME, and put it in the - // lookahead buffer, so this call will match keywords as well. - MUST_MATCH_TOKEN(TOK_NAME, JSMSG_NO_IMPORT_NAME); - Node importName = newName(tokenStream.currentName()); - if (!importName) + if (!tokenStream.peekToken(&tt)) + return null(); + + if (tt == TOK_COMMA) { + if (!tokenStream.getToken(&tt) || !tokenStream.getToken(&tt)) return null(); - if (!tokenStream.getToken(&tt)) + if (tt != TOK_LC && tt != TOK_MUL) { + report(ParseError, false, null(), JSMSG_NAMED_IMPORTS_OR_NAMESPACE_IMPORT); return null(); - if (tt == TOK_NAME && tokenStream.currentName() == context->names().as) { - if (!tokenStream.getToken(&tt)) - return null(); - if (tt != TOK_NAME) { - report(ParseError, false, null(), JSMSG_NO_BINDING_NAME); - return null(); - } - } else { - // Keywords cannot be bound to themselves, so an import name - // that is a keyword is a syntax error if it is not followed - // by the keyword 'as'. - if (IsKeyword(importName->name())) { - JSAutoByteString bytes; - if (!AtomToPrintableString(context, importName->name(), &bytes)) - return null(); - report(ParseError, false, null(), JSMSG_AS_AFTER_RESERVED_WORD, bytes.ptr()); - return null(); - } - tokenStream.ungetToken(); } - Node bindingName = newName(tokenStream.currentName()); - if (!bindingName) - return null(); - Node importSpec = handler.newBinary(PNK_IMPORT_SPEC, importName, bindingName); - if (!importSpec) + if (!namedImportsOrNamespaceImport(tt, importSpecSet)) return null(); - - handler.addList(importSpecSet, importSpec); - - bool matched; - if (!tokenStream.matchToken(&matched, TOK_COMMA)) - return null(); - if (!matched) - break; } - - MUST_MATCH_TOKEN(TOK_RC, JSMSG_RC_AFTER_IMPORT_SPEC_LIST); + } else { + if (!namedImportsOrNamespaceImport(tt, importSpecSet)) + return null(); } if (!tokenStream.getToken(&tt)) return null(); + if (tt != TOK_NAME || tokenStream.currentName() != context->names().from) { - report(ParseError, false, null(), JSMSG_FROM_AFTER_IMPORT_SPEC_SET); + report(ParseError, false, null(), JSMSG_FROM_AFTER_IMPORT_CLAUSE); return null(); } MUST_MATCH_TOKEN(TOK_STRING, JSMSG_MODULE_SPEC_AFTER_FROM); - } else { - if (tt != TOK_STRING) { - report(ParseError, false, null(), JSMSG_DECLARATION_AFTER_IMPORT); - return null(); - } - + } else if (tt == TOK_STRING) { // Handle the form |import 'a'| by leaving the list empty. This is // equivalent to |import {} from 'a'|. importSpecSet->pn_pos.end = importSpecSet->pn_pos.begin; + } else { + report(ParseError, false, null(), JSMSG_DECLARATION_AFTER_IMPORT); + return null(); } Node moduleSpec = stringLiteral(); @@ -4302,8 +4361,7 @@ Parser::importDeclaration() if (!MatchOrInsertSemicolon(tokenStream)) return null(); - return handler.newImportDeclaration(importSpecSet, moduleSpec, - TokenPos(begin, pos().end)); + return handler.newImportDeclaration(importSpecSet, moduleSpec, TokenPos(begin, pos().end)); } template<> diff --git a/js/src/frontend/Parser.h b/js/src/frontend/Parser.h index 2b247f2066d..e443655e47f 100644 --- a/js/src/frontend/Parser.h +++ b/js/src/frontend/Parser.h @@ -635,6 +635,8 @@ class Parser : private JS::AutoGCRooter, public StrictModeGetter Node destructuringExprWithoutYield(YieldHandling yieldHandling, BindData* data, TokenKind tt, unsigned msg); + bool namedImportsOrNamespaceImport(TokenKind tt, Node importSpecSet); + enum ClassContext { ClassStatement, ClassExpression }; Node classDefinition(YieldHandling yieldHandling, ClassContext classContext); diff --git a/js/src/jit-test/tests/modules/import-declaration.js b/js/src/jit-test/tests/modules/import-declaration.js index 54596359a92..cf5468c3f08 100644 --- a/js/src/jit-test/tests/modules/import-declaration.js +++ b/js/src/jit-test/tests/modules/import-declaration.js @@ -38,6 +38,18 @@ program([ ) ]).assert(Reflect.parse("import a from 'b'")); +program([ + importDeclaration( + [ + importSpecifier( + ident("*"), + ident("a") + ) + ], + lit("b") + ) +]).assert(Reflect.parse("import * as a from 'b'")); + program([ importDeclaration( [], @@ -93,6 +105,106 @@ program([ ) ]).assert(Reflect.parse("import { as as as } from 'a'")); +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("a") + ), + importSpecifier( + ident("*"), + ident("b") + ) + ], + lit("c") + ) +]).assert(Reflect.parse("import a, * as b from 'c'")); + +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("d") + ) + ], + lit("a") + ) +]).assert(Reflect.parse("import d, {} from 'a'")); + +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("d") + ), + importSpecifier( + ident("a"), + ident("a") + ) + ], + lit("b") + ) +]).assert(Reflect.parse("import d, { a } from 'b'")); + +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("d") + ), + importSpecifier( + ident("a"), + ident("b") + ) + ], + lit("c") + ) +]).assert(Reflect.parse("import d, { a as b } from 'c'")); + +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("d") + ), + importSpecifier( + ident("a"), + ident("a") + ), + importSpecifier( + ident("b"), + ident("b") + ), + ], + lit("c") + ) +]).assert(Reflect.parse("import d, { a, b } from 'c'")); + +program([ + importDeclaration( + [ + importSpecifier( + ident("default"), + ident("d") + ), + importSpecifier( + ident("a"), + ident("b") + ), + importSpecifier( + ident("c"), + ident("d") + ), + ], + lit("e") + ) +]).assert(Reflect.parse("import d, { a as b, c as d } from 'e'")); + program([ importDeclaration( [ @@ -184,3 +296,43 @@ assertThrowsInstanceOf(function() { assertThrowsInstanceOf(function() { Reflect.parse("import { true } from 'a'"); }, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import a,"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import a, b from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import * as a,"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import * as a, {} from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import as a from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import * a from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import * as from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import , {} from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import d, from 'a'"); +}, SyntaxError); + +assertThrowsInstanceOf(function() { + Reflect.parse("import * as true from 'b'"); +}, SyntaxError); diff --git a/js/src/js.msg b/js/src/js.msg index 3c5ab3cf3ea..dc48d53274a 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -182,6 +182,7 @@ MSG_DEF(JSMSG_ACCESSOR_WRONG_ARGS, 3, JSEXN_SYNTAXERR, "{0} functions must h MSG_DEF(JSMSG_ARGUMENTS_AND_REST, 0, JSEXN_SYNTAXERR, "'arguments' object may not be used in conjunction with a rest parameter") MSG_DEF(JSMSG_ARRAY_COMP_LEFTSIDE, 0, JSEXN_SYNTAXERR, "invalid array comprehension left-hand side") MSG_DEF(JSMSG_ARRAY_INIT_TOO_BIG, 0, JSEXN_INTERNALERR, "array initialiser too large") +MSG_DEF(JSMSG_AS_AFTER_IMPORT_STAR, 0, JSEXN_SYNTAXERR, "missing keyword 'as' after import *") MSG_DEF(JSMSG_AS_AFTER_RESERVED_WORD, 1, JSEXN_SYNTAXERR, "missing keyword 'as' after reserved word '{0}'") MSG_DEF(JSMSG_BAD_ANON_GENERATOR_RETURN, 0, JSEXN_TYPEERR, "anonymous generator function returns a value") MSG_DEF(JSMSG_BAD_ARROW_ARGS, 0, JSEXN_SYNTAXERR, "invalid arrow-function arguments (parentheses around the arrow-function may help)") @@ -249,7 +250,7 @@ MSG_DEF(JSMSG_EMPTY_CONSEQUENT, 0, JSEXN_SYNTAXERR, "mistyped ; after con MSG_DEF(JSMSG_EQUAL_AS_ASSIGN, 0, JSEXN_SYNTAXERR, "test for equality (==) mistyped as assignment (=)?") MSG_DEF(JSMSG_EXPORT_DECL_AT_TOP_LEVEL,0, JSEXN_SYNTAXERR, "export declarations may only appear at top level") MSG_DEF(JSMSG_FINALLY_WITHOUT_TRY, 0, JSEXN_SYNTAXERR, "finally without try") -MSG_DEF(JSMSG_FROM_AFTER_IMPORT_SPEC_SET, 0, JSEXN_SYNTAXERR, "missing keyword 'from' after import specifier set") +MSG_DEF(JSMSG_FROM_AFTER_IMPORT_CLAUSE, 0, JSEXN_SYNTAXERR, "missing keyword 'from' after import clause") MSG_DEF(JSMSG_GARBAGE_AFTER_INPUT, 2, JSEXN_SYNTAXERR, "unexpected garbage after {0}, starting with {1}") MSG_DEF(JSMSG_IDSTART_AFTER_NUMBER, 0, JSEXN_SYNTAXERR, "identifier starts immediately after numeric literal") MSG_DEF(JSMSG_ILLEGAL_CHARACTER, 0, JSEXN_SYNTAXERR, "illegal character") @@ -271,6 +272,8 @@ MSG_DEF(JSMSG_MISSING_OCTAL_DIGITS, 0, JSEXN_SYNTAXERR, "missing octal digits MSG_DEF(JSMSG_MODULES_NOT_IMPLEMENTED, 0, JSEXN_SYNTAXERR, "modules are not implemented yet") MSG_DEF(JSMSG_MODULE_SPEC_AFTER_FROM, 0, JSEXN_SYNTAXERR, "missing module specifier after 'from' keyword") MSG_DEF(JSMSG_NAME_AFTER_DOT, 0, JSEXN_SYNTAXERR, "missing name after . operator") +MSG_DEF(JSMSG_NAME_AFTER_IMPORT_STAR_AS, 0, JSEXN_SYNTAXERR, "missing name after import * as") +MSG_DEF(JSMSG_NAMED_IMPORTS_OR_NAMESPACE_IMPORT, 0, JSEXN_SYNTAXERR, "expected named imports or namespace import after comma") MSG_DEF(JSMSG_NONDEFAULT_FORMAL_AFTER_DEFAULT, 0, JSEXN_SYNTAXERR, "parameter(s) with default followed by parameter without default") MSG_DEF(JSMSG_NO_BINDING_NAME, 0, JSEXN_SYNTAXERR, "missing binding name") MSG_DEF(JSMSG_NO_CLASS_CONSTRUCTOR, 0, JSEXN_TYPEERR, "class default constructors not yet implemented") diff --git a/js/src/jsreflect.cpp b/js/src/jsreflect.cpp index 9807333bcf6..25adf62b1ad 100644 --- a/js/src/jsreflect.cpp +++ b/js/src/jsreflect.cpp @@ -2156,6 +2156,7 @@ bool ASTSerializer::importDeclaration(ParseNode* pn, MutableHandleValue dst) { MOZ_ASSERT(pn->isKind(PNK_IMPORT)); + MOZ_ASSERT(pn->isArity(PN_BINARY)); MOZ_ASSERT(pn->pn_left->isKind(PNK_IMPORT_SPEC_LIST)); MOZ_ASSERT(pn->pn_right->isKind(PNK_STRING)); diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index a2bf63fed05..9d4d1b8cee2 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -187,6 +187,7 @@ macro(signMask, signMask, "signMask") \ macro(source, source, "source") \ macro(stack, stack, "stack") \ + macro(star, star, "*") \ macro(startTimestamp, startTimestamp, "startTimestamp") \ macro(static, static_, "static") \ macro(sticky, sticky, "sticky") \ diff --git a/js/src/vm/Xdr.h b/js/src/vm/Xdr.h index 2bfe458f540..28f39537e19 100644 --- a/js/src/vm/Xdr.h +++ b/js/src/vm/Xdr.h @@ -29,11 +29,11 @@ namespace js { * * https://developer.mozilla.org/en-US/docs/SpiderMonkey/Internals/Bytecode */ -static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 285; +static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 286; static const uint32_t XDR_BYTECODE_VERSION = uint32_t(0xb973c0de - XDR_BYTECODE_VERSION_SUBTRAHEND); -static_assert(JSErr_Limit == 394, +static_assert(JSErr_Limit == 397, "GREETINGS, POTENTIAL SUBTRAHEND INCREMENTER! If you added or " "removed MSG_DEFs from js.msg, you should increment " "XDR_BYTECODE_VERSION_SUBTRAHEND and update this assertion's "