diff --git a/quickjs.c b/quickjs.c index 793a13e9f..133cc1189 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29290,6 +29290,23 @@ static __exception JSAtom js_parse_from_clause(JSParseState *s) return module_name; } +static bool has_unmatched_surrogate(const uint16_t *s, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) { + if (is_lo_surrogate(s[i])) + return true; + if (!is_hi_surrogate(s[i])) + continue; + if (++i == n) + return true; + if (!is_lo_surrogate(s[i])) + return true; + } + return false; +} + static __exception int js_parse_export(JSParseState *s) { JSContext *ctx = s->ctx; @@ -29322,23 +29339,40 @@ static __exception int js_parse_export(JSParseState *s) switch(tok) { case '{': first_export = m->export_entries_count; + bool has_string_binding = false; while (s->token.val != '}') { - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); - return -1; + if (token_is_ident(s->token.val)) { + local_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + local_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (local_name == JS_ATOM_NULL) + return -1; + has_string_binding = true; + } else { + return js_parse_error(s, "identifier or string expected"); } - local_name = JS_DupAtom(ctx, s->token.u.ident.atom); export_name = JS_ATOM_NULL; if (next_token(s)) goto fail; if (token_is_pseudo_keyword(s, JS_ATOM_as)) { if (next_token(s)) goto fail; - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); + if (token_is_ident(s->token.val)) { + export_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + JSString *p = JS_VALUE_GET_STRING(s->token.u.str.str); + if (p->is_wide_char && has_unmatched_surrogate(str16(p), p->len)) { + js_parse_error(s, "illegal export name"); + return -1; + } + export_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (export_name == JS_ATOM_NULL) { + return -1; + } + } else { + js_parse_error(s, "identifier or string expected"); goto fail; } - export_name = JS_DupAtom(ctx, s->token.u.ident.atom); if (next_token(s)) { fail: JS_FreeAtom(ctx, local_name); @@ -29375,6 +29409,9 @@ static __exception int js_parse_export(JSParseState *s) me->export_type = JS_EXPORT_TYPE_INDIRECT; me->u.req_module_idx = idx; } + } else if (has_string_binding) { + // Without 'from' clause, string literals cannot be used as local binding names + return js_parse_error(s, "string export name only allowed with 'from' clause"); } break; case '*': @@ -29382,11 +29419,16 @@ static __exception int js_parse_export(JSParseState *s) /* export ns from */ if (next_token(s)) return -1; - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); - return -1; + if (token_is_ident(s->token.val)) { + export_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + export_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (export_name == JS_ATOM_NULL) { + return -1; + } + } else { + return js_parse_error(s, "identifier or string expected"); } - export_name = JS_DupAtom(ctx, s->token.u.ident.atom); if (next_token(s)) goto fail1; module_name = js_parse_from_clause(s); @@ -29560,11 +29602,15 @@ static __exception int js_parse_import(JSParseState *s) return -1; while (s->token.val != '}') { - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); - return -1; + if (token_is_ident(s->token.val)) { + import_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + import_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (import_name == JS_ATOM_NULL) + return -1; + } else { + return js_parse_error(s, "identifier or string expected expected"); } - import_name = JS_DupAtom(ctx, s->token.u.ident.atom); local_name = JS_ATOM_NULL; if (next_token(s)) goto fail; diff --git a/test262.conf b/test262.conf index 6f756f1bb..130355a9e 100644 --- a/test262.conf +++ b/test262.conf @@ -48,7 +48,7 @@ __proto__ __setter__ AggregateError align-detached-buffer-semantics-with-web-reality -arbitrary-module-namespace-names=skip +arbitrary-module-namespace-names array-find-from-last array-grouping Array.fromAsync diff --git a/tests.conf b/tests.conf index b006e1666..7a2afc6f6 100644 --- a/tests.conf +++ b/tests.conf @@ -8,3 +8,4 @@ tests/empty.js tests/fixture_cyclic_import.js tests/microbench.js tests/test_worker_module.js +tests/fixture_string_exports.js diff --git a/tests/fixture_string_exports.js b/tests/fixture_string_exports.js new file mode 100644 index 000000000..f6439064a --- /dev/null +++ b/tests/fixture_string_exports.js @@ -0,0 +1,12 @@ +// ES2020 string export names test fixture +export const regularExport = "regular"; +const value1 = "value-1"; +const value2 = "value-2"; + +// String export names (ES2020) +export { value1 as "string-export-1" }; +export { value2 as "string-export-2" }; + +// Mixed: regular and string exports +const mixed = "mixed-value"; +export { mixed as normalName, mixed as "string-name" }; diff --git a/tests/test_string_exports.js b/tests/test_string_exports.js new file mode 100644 index 000000000..3198716e7 --- /dev/null +++ b/tests/test_string_exports.js @@ -0,0 +1,25 @@ +// Test ES2020 string export/import names +import { assert } from "./assert.js"; +import * as mod from "./fixture_string_exports.js"; + +// Test string import names +import { "string-export-1" as str1 } from "./fixture_string_exports.js"; +import { "string-export-2" as str2 } from "./fixture_string_exports.js"; +import { "string-name" as strMixed } from "./fixture_string_exports.js"; + +// Test regular imports still work +import { regularExport, normalName } from "./fixture_string_exports.js"; + +// Verify values +assert(str1, "value-1"); +assert(str2, "value-2"); +assert(strMixed, "mixed-value"); +assert(regularExport, "regular"); +assert(normalName, "mixed-value"); + +// Verify module namespace has string-named exports +assert(mod["string-export-1"], "value-1"); +assert(mod["string-export-2"], "value-2"); +assert(mod["string-name"], "mixed-value"); +assert(mod.regularExport, "regular"); +assert(mod.normalName, "mixed-value");