From be3b8064878f2d253e6f1dea41c3ca2174593bb5 Mon Sep 17 00:00:00 2001 From: davidot Date: Mon, 20 Dec 2021 15:29:25 +0100 Subject: [PATCH] LibJS: Parse assert clauses of in- and export statements Based on proposal: https://tc39.es/proposal-import-assertions Since imports are not supported yet this is not functional. --- Userland/Libraries/LibJS/AST.cpp | 23 ++++++-- Userland/Libraries/LibJS/AST.h | 35 ++++++++++--- Userland/Libraries/LibJS/Parser.cpp | 81 ++++++++++++++++++++++++++--- Userland/Libraries/LibJS/Parser.h | 2 + 4 files changed, 124 insertions(+), 17 deletions(-) diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index edbaa72dec..a26e6ac31b 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -3510,6 +3510,16 @@ Value ExportStatement::execute(Interpreter& interpreter, GlobalObject& global_ob return {}; } +static void dump_assert_clauses(ModuleRequest const& request) +{ + if (!request.assertions.is_empty()) { + out("[ "); + for (auto& assertion : request.assertions) + out("{}: {}, ", assertion.key, assertion.value); + out(" ]"); + } +} + void ExportStatement::dump(int indent) const { ASTNode::dump(indent); @@ -3525,7 +3535,12 @@ void ExportStatement::dump(int indent) const for (auto& entry : m_entries) { print_indent(indent + 2); - outln("ModuleRequest: {}, ImportName: {}, LocalName: {}, ExportName: {}", string_or_null(entry.module_request), entry.kind == ExportEntry::ModuleRequest ? string_or_null(entry.local_or_import_name) : "null", entry.kind != ExportEntry::ModuleRequest ? string_or_null(entry.local_or_import_name) : "null", string_or_null(entry.export_name)); + out("ModuleRequest: {}", entry.module_request.module_specifier); + dump_assert_clauses(entry.module_request); + outln(", ImportName: {}, LocalName: {}, ExportName: {}", + entry.kind == ExportEntry::Kind::ModuleRequest ? string_or_null(entry.local_or_import_name) : "null", + entry.kind != ExportEntry::Kind::ModuleRequest ? string_or_null(entry.local_or_import_name) : "null", + string_or_null(entry.export_name)); } } @@ -3535,9 +3550,11 @@ void ImportStatement::dump(int indent) const print_indent(indent + 1); if (m_entries.is_empty()) { // direct from "module" import - outln("Entire module '{}'", m_module_request); + outln("Entire module '{}'", m_module_request.module_specifier); + dump_assert_clauses(m_module_request); } else { - outln("(ExportEntries) from {}", m_module_request); + outln("(ExportEntries) from {}", m_module_request.module_specifier); + dump_assert_clauses(m_module_request); for (auto& entry : m_entries) { print_indent(indent + 2); diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index ea508a5ff6..9c55e214ed 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -217,6 +217,29 @@ private: NonnullRefPtrVector m_functions_hoistable_with_annexB_extension; }; +// 2.9 ModuleRequest Records, https://tc39.es/proposal-import-assertions/#sec-modulerequest-record +struct ModuleRequest { + struct Assertion { + String key; + String value; + }; + + ModuleRequest() = default; + + explicit ModuleRequest(String specifier) + : module_specifier(move(specifier)) + { + } + + void add_assertion(String key, String value) + { + assertions.empend(move(key), move(value)); + } + + String module_specifier; // [[Specifier]] + Vector assertions; // [[Assertions]] +}; + class ImportStatement final : public Statement { public: struct ImportEntry { @@ -224,9 +247,9 @@ public: String local_name; }; - explicit ImportStatement(SourceRange source_range, StringView from_module, Vector entries = {}) + explicit ImportStatement(SourceRange source_range, ModuleRequest from_module, Vector entries = {}) : Statement(source_range) - , m_module_request(from_module) + , m_module_request(move(from_module)) , m_entries(move(entries)) { } @@ -238,14 +261,14 @@ public: bool has_bound_name(StringView name) const; private: - String m_module_request; + ModuleRequest m_module_request; Vector m_entries; }; class ExportStatement final : public Statement { public: struct ExportEntry { - enum Kind { + enum class Kind { ModuleRequest, LocalExport } kind; @@ -253,13 +276,13 @@ public: String export_name; // Only if module request - String module_request; + ModuleRequest module_request; // Has just one of ones below String local_or_import_name; ExportEntry(String export_name, String local_name) - : kind(LocalExport) + : kind(Kind::LocalExport) , export_name(move(export_name)) , local_or_import_name(move(local_name)) { diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 91e643e8fc..b785892cab 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -529,7 +529,7 @@ void Parser::parse_module(Program& program) if (export_statement.has_statement()) continue; for (auto& entry : export_statement.entries()) { - if (entry.kind == ExportStatement::ExportEntry::ModuleRequest) + if (entry.kind == ExportStatement::ExportEntry::Kind::ModuleRequest) return; auto const& exported_name = entry.local_or_import_name; @@ -3912,8 +3912,58 @@ void Parser::check_identifier_name_for_assignment_validity(StringView name, bool } } +bool Parser::match_assert_clause() const +{ + return !m_state.current_token.trivia_contains_line_terminator() && m_state.current_token.original_value() == "assert"sv; +} + +// AssertClause, https://tc39.es/proposal-import-assertions/#prod-AssertClause +void Parser::parse_assert_clause(ModuleRequest& request) +{ + VERIFY(m_state.current_token.original_value() == "assert"sv); + consume(TokenType::Identifier); + consume(TokenType::CurlyOpen); + + while (!done() && !match(TokenType::CurlyClose)) { + String key; + if (match(TokenType::StringLiteral)) { + key = parse_string_literal(m_state.current_token)->value().to_string(); + consume(); + } else if (match_identifier_name()) { + key = consume().value(); + } else { + expected("IdentifierName or StringValue as AssertionKey"); + consume(); + } + + consume(TokenType::Colon); + + if (match(TokenType::StringLiteral)) { + for (auto& entries : request.assertions) { + if (entries.key == key) + syntax_error(String::formatted("Duplicate assertion clauses with name: {}", key)); + } + request.add_assertion(move(key), parse_string_literal(m_state.current_token)->value().to_string()); + } + consume(TokenType::StringLiteral); + + if (match(TokenType::Comma)) + consume(TokenType::Comma); + else + break; + } + + consume(TokenType::CurlyClose); +} + NonnullRefPtr Parser::parse_import_statement(Program& program) { + // We use the extended syntax which adds: + // ImportDeclaration: + // import ImportClause FromClause [no LineTerminator here] AssertClause; + // import ModuleSpecifier [no LineTerminator here] AssertClause; + // From: https://tc39.es/proposal-import-assertions/#prod-ImportDeclaration + auto rule_start = push_start(); if (program.type() != Program::Type::Module) syntax_error("Cannot use import statement outside a module"); @@ -3921,8 +3971,11 @@ NonnullRefPtr Parser::parse_import_statement(Program& program) consume(TokenType::Import); if (match(TokenType::StringLiteral)) { - auto module_name = consume(TokenType::StringLiteral).value(); - return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, module_name); + auto module_request = ModuleRequest(consume(TokenType::StringLiteral).value()); + if (match_assert_clause()) + parse_assert_clause(module_request); + + return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, move(module_request)); } auto match_imported_binding = [&] { @@ -4016,7 +4069,10 @@ NonnullRefPtr Parser::parse_import_statement(Program& program) if (from_statement != "from"sv) syntax_error(String::formatted("Expected 'from' got {}", from_statement)); - auto module_name = consume(TokenType::StringLiteral).value(); + auto module_request = ModuleRequest(consume(TokenType::StringLiteral).value()); + + if (match_assert_clause()) + parse_assert_clause(module_request); Vector entries; entries.ensure_capacity(entries_with_location.size()); @@ -4035,11 +4091,16 @@ NonnullRefPtr Parser::parse_import_statement(Program& program) entries.append(move(entry.entry)); } - return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, module_name, move(entries)); + return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, move(module_request), move(entries)); } NonnullRefPtr Parser::parse_export_statement(Program& program) { + // We use the extended syntax which adds: + // ExportDeclaration: + // export ExportFromClause FromClause [no LineTerminator here] AssertClause ; + // From: https://tc39.es/proposal-import-assertions/#prod-ExportDeclaration + auto rule_start = push_start(); if (program.type() != Program::Type::Module) syntax_error("Cannot use export statement outside a module"); @@ -4062,10 +4123,10 @@ NonnullRefPtr Parser::parse_export_statement(Program& program) ExportStatement::ExportEntry entry; Position position; - void to_module_request(String from_module) + void to_module_request(ModuleRequest from_module) { entry.kind = ExportStatement::ExportEntry::Kind::ModuleRequest; - entry.module_request = from_module; + entry.module_request = move(from_module); } }; @@ -4200,7 +4261,11 @@ NonnullRefPtr Parser::parse_export_statement(Program& program) if (check_for_from != NotAllowed && match_from()) { consume(TokenType::Identifier); if (match(TokenType::StringLiteral)) { - auto from_specifier = consume().value(); + auto from_specifier = ModuleRequest(consume().value()); + + if (match_assert_clause()) + parse_assert_clause(from_specifier); + for (auto& entry : entries_with_location) entry.to_module_request(from_specifier); } else { diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index 7153f36aa9..0d00e652de 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -185,6 +185,7 @@ private: bool match_secondary_expression(const Vector& forbidden = {}) const; bool match_statement() const; bool match_export_or_import() const; + bool match_assert_clause() const; bool match_declaration() const; bool try_match_let_declaration() const; bool match_variable_declaration() const; @@ -220,6 +221,7 @@ private: bool parse_directive(ScopeNode& body); void parse_statement_list(ScopeNode& output_node, AllowLabelledFunction allow_labelled_functions = AllowLabelledFunction::No); + void parse_assert_clause(ModuleRequest& request); struct RulePosition { AK_MAKE_NONCOPYABLE(RulePosition);