From 0a9885e10f1cd9085a1a24bf2f2aa29e1d6bd75f Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 5 Oct 2026 20:55:51 +0900 Subject: [PATCH 1/2] fix(parser): preserve export tokens and empty reexports Object-literal detection records getter and async prefixes before checking the following token, so it returns phantom export names. The JavaScript identifier check compares a numeric code point with a string, and Wasm permits a leading identifier escape. The Wasm adapter also discards empty reexport strings through a truthiness check. Signed-off-by: Ruben Bridgewater --- lexer.js | 8 ++++-- lib/lexer.wasm | Bin 22591 -> 22617 bytes src/lexer.c | 6 ++++- src/lexer.js | 2 +- test/_unit.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lexer.js b/lexer.js index ebcacd8..0911bc1 100755 --- a/lexer.js +++ b/lexer.js @@ -1030,6 +1030,10 @@ function tryParseLiteralExports () { } ch = source.charCodeAt(pos); } + else if (ch !== 44/*,*/ && ch !== 125/*}*/ && ch !== 61/*=*/ && ch !== 40/*(*/) { + pos = revertPos; + return; + } _exports.add(decode(source.slice(startPos, endPos))); } else if (ch === 46/*.*/ && source.startsWith('..', pos + 1)) { @@ -1330,14 +1334,14 @@ function isIdentifierChar(code, astral) { function identifier () { let ch = source.codePointAt(pos); - if (!isIdentifierStart(ch, true) || ch === '\\') + if (!isIdentifierStart(ch, true)) return false; pos += codePointLen(ch); while (ch = source.codePointAt(pos)) { if (isIdentifierChar(ch, true)) { pos += codePointLen(ch); } - else if (ch === '\\') { + else if (ch === 92/*\*/) { // no identifier escapes support for now return false; } diff --git a/lib/lexer.wasm b/lib/lexer.wasm index 4917a0ed79b1ff82d734ffbaea05213b3cf62a32..f113de8b5d402468b80d2683be899b3607a68812 100755 GIT binary patch delta 184 zcmdnLf$`=B#tkNnjNdk!GMcF}-rAg_7SE{9sGs zw94jVVPs=tV&DdH{xZ1nGV-ua-oP!g`MG)kUs5h3V?9&7BZC5ifRYk}fUqksgOuZ* z1&kU@4312WEI=&{4J@A0S&EEI4$KZ983jf&CI$sYeMYbjS6)V-Rv{J#1x7UWJdE7j MAage_Gn=Uh0M@A^4FCWD delta 175 zcmcb)fpPx^#tkNnjCVGhGMcF}Zr+@u7SG5jAgILPXywM@FnJ@l@Mb=Z0KWPvM#g#t z1_31{Mgd_(2BvxiM#no0?koAjrXsQc diff --git a/src/lexer.c b/src/lexer.c index 67a8718..a2aca31 100755 --- a/src/lexer.c +++ b/src/lexer.c @@ -1033,6 +1033,10 @@ void tryParseLiteralExports () { } ch = *pos; } + else if (ch != ',' && ch != '}' && ch != '=' && ch != '(') { + pos = revertPos; + return; + } addExport(startPos, endPos); } else if (ch == '\'' || ch == '"') { @@ -1181,7 +1185,7 @@ uint32_t fullCharCode(uint16_t ch) { bool identifier (uint16_t startCh) { uint32_t ch = fullCharCode(startCh); - if (!isIdentifierStart(ch) && ch != '\\') + if (!isIdentifierStart(ch)) return false; pos += charCodeByteLen(ch); while (ch = fullCharCode(*pos)) { diff --git a/src/lexer.js b/src/lexer.js index 13e8592..2617997 100755 --- a/src/lexer.js +++ b/src/lexer.js @@ -44,7 +44,7 @@ export function parse (source, name = '@', options) { while (wasm.rre()) { const reexptStr = decode(source.slice(wasm.res(), wasm.ree())); - if (reexptStr) + if (reexptStr !== undefined) reexports.add(reexptStr); } while (wasm.ru()) { diff --git a/test/_unit.js b/test/_unit.js index 3fea1e6..0b6af49 100755 --- a/test/_unit.js +++ b/test/_unit.js @@ -177,6 +177,71 @@ suite('Lexer', () => { } }); + test('does not detect method prefixes as object exports', () => { + for (const property of ['get value () { return value; }', 'set value (value) {}', + 'async value () {}', 'async *value () {}', + 'get "value" () { return value; }', 'get [value] () { return value; }', + 'get /* comment */ value () { return value; }']) { + assert.deepStrictEqual(parse(`module.exports = { before, ${property}, after };`), { + exports: ['before'], reexports: [] + }); + } + for (const property of ['get', 'get: value', '"get": value', "'get': value"]) { + assert.deepStrictEqual(parse(`module.exports = { ${property}, after };`), { + exports: ['get', 'after'], reexports: [] + }); + } + for (const name of ['value', 'get', 'set', 'async']) { + assert.deepStrictEqual(parse(`module.exports = { before, ${name} () {}, after };`), { + exports: ['before', name], reexports: [] + }); + } + }); + + test('does not detect escaped identifiers', () => { + for (const name of ['\\u0061', '\\u{61}', 'a\\u0062', 'a\\u{62}']) { + assert.deepStrictEqual(parse(`exports.${name} = value; module.exports = { ${name} };`), { + exports: [], reexports: [] + }); + } + assert.deepStrictEqual(parse('exports.α = value; exports.𐊧 = value;'), { + exports: ['α', '𐊧'], reexports: [] + }); + assert.deepStrictEqual(parse('exports["\\u0061"] = value; module.exports = {"\\u0062": value};'), { + exports: ['a', 'b'], reexports: [] + }); + }); + + test('preserves empty reexport specifiers', () => { + for (const source of ['module.exports = require("");', "module.exports = require('');", + '__exportStar(require(""), exports);', 'module.exports = { ...require("") };']) { + assert.deepStrictEqual(parse(source), { exports: [], reexports: [''] }); + } + assert.deepStrictEqual(parse('module.exports = require("\\uD800");'), { + exports: [], reexports: [] + }); + }); + + test('does not detect escaped identifier values or getters', () => { + for (const name of ['\\u0061', '\\u{61}', 'a\\u0062', 'a\\u{62}']) { + for (const key of ['value', '"value"']) { + assert.deepStrictEqual(parse(`module.exports = { ${key}: ${name} };`), { + exports: [], reexports: [] + }); + } + const getter = `Object.defineProperty(exports, 'value', { get () { return ${name}; } });`; + assert.deepStrictEqual(parse(getter), { + exports: [], reexports: [] + }); + assert.deepStrictEqual(parse(`exports.value = value; ${getter}`), { + exports: [], reexports: [] + }); + } + assert.deepStrictEqual(parse("Object.defineProperty(exports, 'value', { get () { return module['\\u0061']; } });"), { + exports: ['value'], reexports: [] + }); + }); + test('automatic Wasm initialization', async () => { const lexer = await import('../dist/lexer.mjs?automatic-initialization'); From 2a27840ffa3a8dad855d01069ca7fc96822279e1 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 11 Oct 2026 12:20:14 +0900 Subject: [PATCH 2/2] fix(parser): retain export keys before escaped values An identifier escape in an object property value stops detection before the parser records its key. Keep the key and stop detecting later properties, as with member expressions. Signed-off-by: Ruben Bridgewater --- lexer.js | 4 ++-- lib/lexer.wasm | Bin 22617 -> 22639 bytes src/lexer.c | 4 ++-- test/_unit.js | 18 +++++++++++++----- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lexer.js b/lexer.js index 0911bc1..7f0ef63 100755 --- a/lexer.js +++ b/lexer.js @@ -1024,7 +1024,7 @@ function tryParseLiteralExports () { pos++; ch = commentWhitespace(); // nothing more complex than identifier expressions for now - if (!identifier()) { + if (!identifier() && source.charCodeAt(pos) !== 92/*\*/) { pos = revertPos; return; } @@ -1056,7 +1056,7 @@ function tryParseLiteralExports () { pos++; ch = commentWhitespace(); // nothing more complex than identifier expressions for now - if (!identifier()) { + if (!identifier() && source.charCodeAt(pos) !== 92/*\*/) { pos = revertPos; return; } diff --git a/lib/lexer.wasm b/lib/lexer.wasm index f113de8b5d402468b80d2683be899b3607a68812..4c7fc183a5b392d2649432c105a5b80e749a7619 100755 GIT binary patch delta 89 zcmcb)f${wY#tkNnOg+;#n=zWIvV34=WZ9grwvbW5anAxq4JHPCMg~O&UIqmQ$N&GC g3mxwOc|gLQmu0fNMw&1}krG(Ake7M$WQ}9`0I5 { }); }); - test('does not detect escaped identifier values or getters', () => { - for (const name of ['\\u0061', '\\u{61}', 'a\\u0062', 'a\\u{62}']) { - for (const key of ['value', '"value"']) { - assert.deepStrictEqual(parse(`module.exports = { ${key}: ${name} };`), { - exports: [], reexports: [] + test('preserves export keys before unsupported identifier value expressions', () => { + for (const value of ['a.b', '\\u0061', '\\u{61}', 'a\\u0062', 'a\\u{62}']) { + for (const key of ['value', '"value"', "'value'"]) { + assert.deepStrictEqual(parse(`module.exports = { ${key}: ${value} };`), { + exports: ['value'], reexports: [] + }); + assert.deepStrictEqual(parse(`module.exports = { before, ${key}: ${value}, after }; exports.later = later;`), { + exports: ['before', 'value', 'later'], reexports: [] }); } + } + }); + + test('does not detect getters with escaped identifier returns', () => { + for (const name of ['\\u0061', '\\u{61}', 'a\\u0062', 'a\\u{62}']) { const getter = `Object.defineProperty(exports, 'value', { get () { return ${name}; } });`; assert.deepStrictEqual(parse(getter), { exports: [], reexports: []