Repository: boa-dev/boa (rust)

Issue:
Uncaught SyntaxError when export alias is null
When running an ES module with the following contents:

```javascript
const null_ = () => 42;
export { null_ as null };
```

This results in a parsing error:

```
Uncaught SyntaxError: expected one of 'identifier' or 'string literal', got 'null' in export specifier parsing at line 2, col 19
```

**To Reproduce**

This JavaScript code reproduces the issue:

```javascript
const null_ = () => 42;
export { null_ as null };
```

Save the file as, e.g. `module.mjs`. Run with the boa CLI:

```
boa -m ./module.mjs
```

**Expected behavior**

Boa should parse and execute the file as intended.

**Build environment (please complete the following information):**

- OS: macOS Monterey
- Version: 12.7.6

**Additional context**

I wanted to use `https://github.com/badrap/valita` for a project and Boa wasn't able to parse it.


Pull request:
Allow bool and null literals in export aliases
Fixes #4110


Patch:
diff --git a/core/ast/src/keyword/mod.rs b/core/ast/src/keyword/mod.rs
index f1c36ec1..a781f77c 100644
--- a/core/ast/src/keyword/mod.rs
+++ b/core/ast/src/keyword/mod.rs
@@ -196,16 +196,6 @@ pub enum Keyword {
     /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends
     Extends,
 
-    /// The `false` keyword.
-    ///
-    /// More information:
-    ///  - [ECMAScript reference][spec]
-    ///  - [MDN documentation][mdn]
-    ///
-    /// [spec]: https://tc39.es/ecma262/#prod-BooleanLiteral
-    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
-    False,
-
     /// The `finally` keyword.
     ///
     /// More information:
@@ -308,16 +298,6 @@ pub enum Keyword {
     /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
     New,
 
-    /// The `null` keyword.
-    ///
-    /// More information:
-    ///  - [ECMAScript reference][spec]
-    ///  - [MDN documentation][mdn]
-    ///
-    /// [spec]: https://tc39.es/ecma262/#prod-NullLiteral
-    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null
-    Null,
-
     /// The `of` keyword.
     ///
     /// More information:
@@ -386,16 +366,6 @@ pub enum Keyword {
     /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
     Throw,
 
-    /// The `true` keyword
-    ///
-    /// More information:
-    ///  - [ECMAScript reference][spec]
-    ///  - [MDN documentation][mdn]
-    ///
-    /// [spec]: https://tc39.es/ecma262/#prod-BooleanLiteral
-    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
-    True,
-
     /// The `try` keyword.
     ///
     /// More information:
@@ -509,7 +479,6 @@ impl Keyword {
             Self::Enum => ("enum", utf16!("enum")),
             Self::Extends => ("extends", utf16!("extends")),
             Self::Export => ("export", utf16!("export")),
-            Self::False => ("false", utf16!("false")),
             Self::Finally => ("finally", utf16!("finally")),
             Self::For => ("for", utf16!("for")),
             Self::Function => ("function", utf16!("function")),
@@ -519,14 +488,12 @@ impl Keyword {
             Self::Import => ("import", utf16!("import")),
             Self::Let => ("let", utf16!("let")),
             Self::New => ("new", utf16!("new")),
-            Self::Null => ("null", utf16!("null")),
             Self::Of => ("of", utf16!("of")),
             Self::Return => ("return", utf16!("return")),
             Self::Super => ("super", utf16!("super")),
             Self::Switch => ("switch", utf16!("switch")),
             Self::This => ("this", utf16!("this")),
             Self::Throw => ("throw", utf16!("throw")),
-            Self::True => ("true", utf16!("true")),
             Self::Try => ("try", utf16!("try")),
             Self::TypeOf => ("typeof", utf16!("typeof")),
             Self::Var => ("var", utf16!("var")),
@@ -557,7 +524,6 @@ impl Keyword {
             Self::Enum => Sym::ENUM,
             Self::Export => Sym::EXPORT,
             Self::Extends => Sym::EXTENDS,
-            Self::False => Sym::FALSE,
             Self::Finally => Sym::FINALLY,
             Self::For => Sym::FOR,
             Self::Function => Sym::FUNCTION,
@@ -567,14 +533,12 @@ impl Keyword {
             Self::Import => Sym::IMPORT,
             Self::Let => Sym::LET,
             Self::New => Sym::NEW,
-            Self::Null => Sym::NULL,
             Self::Of => Sym::OF,
             Self::Return => Sym::RETURN,
             Self::Super => Sym::SUPER,
             Self::Switch => Sym::SWITCH,
             Self::This => Sym::THIS,
             Self::Throw => Sym::THROW,
-            Self::True => Sym::TRUE,
             Self::Try => Sym::TRY,
             Self::TypeOf => Sym::TYPEOF,
             Self::Var => Sym::VAR,
@@ -629,7 +593,6 @@ impl FromStr for Keyword {
             "enum" => Ok(Self::Enum),
             "extends" => Ok(Self::Extends),
             "export" => Ok(Self::Export),
-            "false" => Ok(Self::False),
             "finally" => Ok(Self::Finally),
             "for" => Ok(Self::For),
             "function" => Ok(Self::Function),
@@ -639,14 +602,12 @@ impl FromStr for Keyword {
             "import" => Ok(Self::Import),
             "let" => Ok(Self::Let),
             "new" => Ok(Self::New),
-            "null" => Ok(Self::Null),
             "of" => Ok(Self::Of),
             "return" => Ok(Self::Return),
             "super" => Ok(Self::Super),
             "switch" => Ok(Self::Switch),
             "this" => Ok(Self::This),
             "throw" => Ok(Self::Throw),
-            "true" => Ok(Self::True),
             "try" => Ok(Self::Try),
             "typeof" => Ok(Self::TypeOf),
             "var" => Ok(Self::Var),
diff --git a/core/parser/src/lexer/identifier.rs b/core/parser/src/lexer/identifier.rs
index 6251a18b..35ac699d 100644
--- a/core/parser/src/lexer/identifier.rs
+++ b/core/parser/src/lexer/identifier.rs
@@ -4,7 +4,7 @@ use crate::lexer::{
     token::ContainsEscapeSequence, Cursor, Error, StringLiteral, Token, TokenKind, Tokenizer,
 };
 use crate::source::ReadChar;
-use boa_ast::{Keyword, Position, Span};
+use boa_ast::{Position, Span};
 use boa_interner::Interner;
 use boa_profiler::Profiler;
 
@@ -68,17 +68,17 @@ impl<R> Tokenizer<R> for Identifier {
             Self::take_identifier_name(cursor, start_pos, self.init)?;
 
         let token_kind = match identifier_name.parse() {
-            Ok(Keyword::True) => {
+            Ok(keyword) => TokenKind::Keyword((keyword, contains_escaped_chars)),
+            Err(_) if identifier_name == "true" => {
                 TokenKind::BooleanLiteral((true, ContainsEscapeSequence(contains_escaped_chars)))
             }
-            Ok(Keyword::False) => {
+            Err(_) if identifier_name == "false" => {
                 TokenKind::BooleanLiteral((false, ContainsEscapeSequence(contains_escaped_chars)))
             }
-            Ok(Keyword::Null) => {
+            Err(_) if identifier_name == "null" => {
                 TokenKind::NullLiteral(ContainsEscapeSequence(contains_escaped_chars))
             }
-            Ok(keyword) => TokenKind::Keyword((keyword, contains_escaped_chars)),
-            _ => TokenKind::IdentifierName((
+            Err(_) => TokenKind::IdentifierName((
                 interner.get_or_intern(identifier_name.as_str()),
                 ContainsEscapeSequence(contains_escaped_chars),
             )),
diff --git a/core/parser/src/parser/statement/declaration/export.rs b/core/parser/src/parser/statement/declaration/export.rs
index dcca0ec8..7f23c9f2 100644
--- a/core/parser/src/parser/statement/declaration/export.rs
+++ b/core/parser/src/parser/statement/declaration/export.rs
@@ -317,6 +317,10 @@ where
             }
             TokenKind::IdentifierName((ident, _)) => Ok((*ident, false)),
             TokenKind::Keyword((kw, _)) => Ok((kw.to_sym(), false)),
+            TokenKind::BooleanLiteral((b, _)) => {
+                Ok((if *b { Sym::TRUE } else { Sym::FALSE }, false))
+            }
+            TokenKind::NullLiteral(_) => Ok((Sym::NULL, false)),
             _ => Err(Error::expected(
                 ["identifier".to_owned(), "string literal".to_owned()],
                 tok.to_string(interner),


Test patch:
diff --git a/core/ast/src/keyword/tests.rs b/core/ast/src/keyword/tests.rs
index 182f685e..ec3cc6bf 100644
--- a/core/ast/src/keyword/tests.rs
+++ b/core/ast/src/keyword/tests.rs
@@ -21,7 +21,6 @@ fn all_keywords() -> impl Iterator<Item = Keyword> {
         Keyword::Enum,
         Keyword::Export,
         Keyword::Extends,
-        Keyword::False,
         Keyword::Finally,
         Keyword::For,
         Keyword::Function,
@@ -31,14 +30,12 @@ fn all_keywords() -> impl Iterator<Item = Keyword> {
         Keyword::Import,
         Keyword::Let,
         Keyword::New,
-        Keyword::Null,
         Keyword::Of,
         Keyword::Return,
         Keyword::Super,
         Keyword::Switch,
         Keyword::This,
         Keyword::Throw,
-        Keyword::True,
         Keyword::Try,
         Keyword::TypeOf,
         Keyword::Var,
@@ -135,10 +132,6 @@ fn as_str() {
                 assert_eq!(k, Keyword::Export);
                 assert_eq!(utf16, utf16!("export"));
             }
-            ("false", utf16) => {
-                assert_eq!(k, Keyword::False);
-                assert_eq!(utf16, utf16!("false"));
-            }
             ("finally", utf16) => {
                 assert_eq!(k, Keyword::Finally);
                 assert_eq!(utf16, utf16!("finally"));
@@ -175,10 +168,6 @@ fn as_str() {
                 assert_eq!(k, Keyword::New);
                 assert_eq!(utf16, utf16!("new"));
             }
-            ("null", utf16) => {
-                assert_eq!(k, Keyword::Null);
-                assert_eq!(utf16, utf16!("null"));
-            }
             ("of", utf16) => {
                 assert_eq!(k, Keyword::Of);
                 assert_eq!(utf16, utf16!("of"));
@@ -203,10 +192,6 @@ fn as_str() {
                 assert_eq!(k, Keyword::Throw);
                 assert_eq!(utf16, utf16!("throw"));
             }
-            ("true", utf16) => {
-                assert_eq!(k, Keyword::True);
-                assert_eq!(utf16, utf16!("true"));
-            }
             ("try", utf16) => {
                 assert_eq!(k, Keyword::Try);
                 assert_eq!(utf16, utf16!("try"));
@@ -260,7 +245,6 @@ fn to_sym() {
             Sym::ENUM => assert_eq!(k, Keyword::Enum),
             Sym::EXPORT => assert_eq!(k, Keyword::Export),
             Sym::EXTENDS => assert_eq!(k, Keyword::Extends),
-            Sym::FALSE => assert_eq!(k, Keyword::False),
             Sym::FINALLY => assert_eq!(k, Keyword::Finally),
             Sym::FOR => assert_eq!(k, Keyword::For),
             Sym::FUNCTION => assert_eq!(k, Keyword::Function),
@@ -270,14 +254,12 @@ fn to_sym() {
             Sym::IMPORT => assert_eq!(k, Keyword::Import),
             Sym::LET => assert_eq!(k, Keyword::Let),
             Sym::NEW => assert_eq!(k, Keyword::New),
-            Sym::NULL => assert_eq!(k, Keyword::Null),
             Sym::OF => assert_eq!(k, Keyword::Of),
             Sym::RETURN => assert_eq!(k, Keyword::Return),
             Sym::SUPER => assert_eq!(k, Keyword::Super),
             Sym::SWITCH => assert_eq!(k, Keyword::Switch),
             Sym::THIS => assert_eq!(k, Keyword::This),
             Sym::THROW => assert_eq!(k, Keyword::Throw),
-            Sym::TRUE => assert_eq!(k, Keyword::True),
             Sym::TRY => assert_eq!(k, Keyword::Try),
             Sym::TYPEOF => assert_eq!(k, Keyword::TypeOf),
             Sym::VAR => assert_eq!(k, Keyword::Var),
@@ -311,7 +293,6 @@ fn try_into_binary_op() {
             | Keyword::Enum
             | Keyword::Export
             | Keyword::Extends
-            | Keyword::False
             | Keyword::Finally
             | Keyword::For
             | Keyword::Function
@@ -319,14 +300,12 @@ fn try_into_binary_op() {
             | Keyword::Import
             | Keyword::Let
             | Keyword::New
-            | Keyword::Null
             | Keyword::Of
             | Keyword::Return
             | Keyword::Super
             | Keyword::Switch
             | Keyword::This
             | Keyword::Throw
-            | Keyword::True
             | Keyword::Try
             | Keyword::TypeOf
             | Keyword::Var
diff --git a/core/parser/src/parser/statement/declaration/tests.rs b/core/parser/src/parser/statement/declaration/tests.rs
index 07824814..054c45d5 100644
--- a/core/parser/src/parser/statement/declaration/tests.rs
+++ b/core/parser/src/parser/statement/declaration/tests.rs
@@ -1,8 +1,10 @@
-use crate::parser::tests::{check_invalid_script, check_script_parser};
+use crate::parser::tests::{check_invalid_script, check_module_parser, check_script_parser};
 use boa_ast::{
-    declaration::{LexicalDeclaration, VarDeclaration, Variable},
+    declaration::{
+        ExportDeclaration, ExportSpecifier, LexicalDeclaration, VarDeclaration, Variable,
+    },
     expression::literal::Literal,
-    Declaration, Statement,
+    Declaration, ModuleItem, Statement,
 };
 use boa_interner::{Interner, Sym};
 use boa_macros::utf16;
@@ -360,3 +362,38 @@ fn lexical_declaration_early_errors() {
     check_invalid_script("for (let a = 0, a = 0; ; ) {}");
     check_invalid_script("for (const a = 0, a = 0; ; ) {}");
 }
+
+/// Checks module exports with reserved keywords
+#[test]
+fn module_export_reserved() {
+    let interner = &mut Interner::default();
+    let val = interner.get_or_intern_static("val", utf16!("val"));
+    check_module_parser(
+        r#"
+            const val = null;
+            export { val as null, val as true, val as false };
+        "#,
+        vec![
+            ModuleItem::StatementListItem(
+                Declaration::Lexical(LexicalDeclaration::Const(
+                    vec![Variable::from_identifier(
+                        val.into(),
+                        Some(Literal::Null.into()),
+                    )]
+                    .try_into()
+                    .unwrap(),
+                ))
+                .into(),
+            ),
+            ModuleItem::ExportDeclaration(ExportDeclaration::List(
+                vec![
+                    ExportSpecifier::new(Sym::NULL, val, false),
+                    ExportSpecifier::new(Sym::TRUE, val, false),
+                    ExportSpecifier::new(Sym::FALSE, val, false),
+                ]
+                .into(),
+            )),
+        ],
+        interner,
+    );
+}
diff --git a/core/parser/src/parser/tests/mod.rs b/core/parser/src/parser/tests/mod.rs
index 5cc9eb28..c3231252 100644
--- a/core/parser/src/parser/tests/mod.rs
+++ b/core/parser/src/parser/tests/mod.rs
@@ -24,7 +24,8 @@ use boa_ast::{
     },
     scope::Scope,
     statement::{If, Return},
-    Expression, Script, Statement, StatementList, StatementListItem,
+    Expression, Module, ModuleItem, ModuleItemList, Script, Statement, StatementList,
+    StatementListItem,
 };
 use boa_interner::Interner;
 use boa_macros::utf16;
@@ -46,6 +47,23 @@ where
     );
 }
 
+/// Checks that the given JavaScript string gives the expected expression.
+#[track_caller]
+pub(super) fn check_module_parser<L>(js: &str, expr: L, interner: &mut Interner)
+where
+    L: Into<Box<[ModuleItem]>>,
+{
+    let mut module = Module::new(ModuleItemList::from(expr.into()));
+    let scope = Scope::new_global();
+    module.analyze_scope(&scope, interner);
+    assert_eq!(
+        Parser::new(Source::from_bytes(js))
+            .parse_module(&Scope::new_global(), interner)
+            .expect("failed to parse"),
+        module,
+    );
+}
+
 /// Checks that the given javascript string creates a parse error.
 #[track_caller]
 pub(super) fn check_invalid_script(js: &str) {