From 2565391b007606c9f6d212f97ec849b0397f6cb0 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 22419 -> 22445 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 3e30b6d..9822535 100755 --- a/lexer.js +++ b/lexer.js @@ -1005,6 +1005,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)) { @@ -1305,14 +1309,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 dc291380c4735361e2b391d5795f8db0ea8131fd..da4c8dd8630f968e1043eaf97255931264560c7f 100755 GIT binary patch delta 185 zcmbQdo^kDZ#tqVpjE^_VFz#ezytVlnQ#_*qlLM23fPfN%qeeC#Dn1_2>gUIr=0 zJqs8$m>3+H99e*R8X8zUrLzUkKs Nxk2V`77%WB0RWhfA-n(p delta 172 zcmZ3xo^kSe#tqVpj3+nCFz#ez+`Rc3Q#>P^fPfN%qt)aDbCJzuEJ1wrRg8@F3Jd}Y zN{j+RiVRHk3XG0-7~EML92pcCTzMI#xE=Q_VANn@aAb010m?Nruy{&m0TnthJAh;q u7|fV}D)kw`MgTP^FbW8=I4Gd2=V9dL1{w)6+zo6hkUyCb=qfAWViy1c3n7{S diff --git a/src/lexer.c b/src/lexer.c index fd3f1cf..3e53b4f 100755 --- a/src/lexer.c +++ b/src/lexer.c @@ -1013,6 +1013,10 @@ void tryParseLiteralExports () { } ch = *pos; } + else if (ch != ',' && ch != '}' && ch != '=' && ch != '(') { + pos = revertPos; + return; + } addExport(startPos, endPos); } else if (ch == '\'' || ch == '"') { @@ -1161,7 +1165,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 431cb5b..599540c 100755 --- a/src/lexer.js +++ b/src/lexer.js @@ -42,7 +42,7 @@ export function parse (source, name = '@') { 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 719d922..ccca8e7 100755 --- a/test/_unit.js +++ b/test/_unit.js @@ -88,6 +88,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 dc088cc20efd9ad66c5d76c8de7dc9e804774c9c 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 22445 -> 22467 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 9822535..10fefbc 100755 --- a/lexer.js +++ b/lexer.js @@ -999,7 +999,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; } @@ -1031,7 +1031,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 da4c8dd8630f968e1043eaf97255931264560c7f..78db5f14c34310c7077d069dbeaaac7b8b32ab62 100755 GIT binary patch delta 88 zcmZ3xp7HQ{#tqVpj6XNaFz#ez`M}D^viT;{LPiD0Jqs8$m>BdK859|K859^C|Nmz$ gbi4!P0SR|rmdTwgX~GCaN?_qaUgphzSvCX#0HCB9X#fBK delta 66 zcmX@So^kDZ#tqVpjE^_VFz#ezxy8!Jy!j^6LPma9UKYna3m7$+81xw#6d5MZVoBrU NWkwd-EX=wg2mq9F5jy|? diff --git a/src/lexer.c b/src/lexer.c index 3e53b4f..1745aca 100755 --- a/src/lexer.c +++ b/src/lexer.c @@ -1007,7 +1007,7 @@ void tryParseLiteralExports () { pos++; ch = commentWhitespace(); // nothing more complex than identifier expressions for now - if (!identifier(ch)) { + if (!identifier(ch) && *pos != '\\') { pos = revertPos; return; } @@ -1028,7 +1028,7 @@ void tryParseLiteralExports () { pos++; ch = commentWhitespace(); // nothing more complex than identifier expressions for now - if (!identifier(ch)) { + if (!identifier(ch) && *pos != '\\') { pos = revertPos; return; } diff --git a/test/_unit.js b/test/_unit.js index ccca8e7..5a794ee 100755 --- a/test/_unit.js +++ b/test/_unit.js @@ -133,13 +133,21 @@ suite('Lexer', () => { }); }); - 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: []