Sitelet https://github.com/guybedford/es-module-lexer/commit/d61e97a651b595453bc52c32c5485832771bdbb3
Skip to content

Commit d61e97a

Browse files
authored
feat: expose export statement start position as ss (#206)
Exports exposed only the exported and local name ranges, so a consumer could not locate the `export` keyword introducing a binding. Each export specifier now carries `ss`, the statement start, matching the import `ss`. Only the statement start is tracked, not its end. Fixes: #112
1 parent 064d0db commit d61e97a

8 files changed

Lines changed: 80 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ import { init, parse } from 'es-module-lexer';
108108
source.slice(exports[1].s, exports[1].e);
109109
// Returns "q"
110110
source.slice(exports[1].ls, exports[1].le);
111+
112+
// "ss" = export statement start (only the start is tracked, not the end)
113+
// Returns "export"
114+
source.slice(exports[0].ss, exports[0].ss + 6);
115+
111116
// Returns "'external name'"
112117
source.slice(exports[2].s, exports[2].e);
113118
// Returns -1

‎chompfile.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ run = """
101101
${{ EMSDK_PATH }}/emsdk activate 6.0.0
102102
103103
${{ EMSDK_PATH }}/upstream/emscripten/emcc src/lexer.c -o lib/lexer.wasm -Oz -flto --no-entry -s STANDALONE_WASM=1 \
104-
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','___heap_base']" \
104+
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_ess','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','___heap_base']" \
105105
-s SUPPORT_LONGJMP=0 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -Wno-logical-op-parentheses -Wno-parentheses
106106
"""
107107

@@ -114,7 +114,7 @@ run = """
114114
${{ EMSDK_FASTCOMP_PATH }}/emsdk activate 1.40.1-fastcomp
115115
116116
${{ EMSDK_FASTCOMP_PATH }}/fastcomp/emscripten/emcc ./src/lexer.c -o lib/lexer.emcc.js -s WASM=0 -Oz --closure 1 \
117-
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','_setSource']" \
117+
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_ess','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','_setSource']" \
118118
-s AGGRESSIVE_VARIABLE_ELIMINATION=1 -s GLOBAL_BASE=8 \
119119
-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s TOTAL_STACK=4997968 -s --separate-asm -Wno-logical-op-parentheses -Wno-parentheses
120120
"""
@@ -134,7 +134,7 @@ run = """
134134
${{ EMSDK_FASTCOMP_PATH }}/emsdk activate 1.40.1-fastcomp
135135
136136
${{ EMSDK_FASTCOMP_PATH }}/fastcomp/emscripten/emcc ./src/lexer.c -o lib/lexer.layout.js -s WASM=0 -Oz --closure 0 \
137-
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','_setSource']" \
137+
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_it','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_ess','_f','_ms','_ra','_rsa','_aks','_ake','_avs','_ave','_setSource']" \
138138
-s AGGRESSIVE_VARIABLE_ELIMINATION=1 -s GLOBAL_BASE=8 \
139139
-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s TOTAL_STACK=4997968 -Wno-logical-op-parentheses -Wno-parentheses
140140
"""

‎lexer.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let source, pos, end,
1212
templateStack,
1313
imports,
1414
exports,
15+
exportStatementStart,
1516
name;
1617

1718
function addImport (ss, s, e, d) {
@@ -26,6 +27,7 @@ function addExport (s, e, ls, le) {
2627
e,
2728
ls,
2829
le,
30+
ss: exportStatementStart,
2931
n: s[0] === '"' ? readString(s, '"') : s[0] === "'" ? readString(s, "'") : source.slice(s, e),
3032
ln: ls[0] === '"' ? readString(ls, '"') : ls[0] === "'" ? readString(ls, "'") : source.slice(ls, le)
3133
});
@@ -339,9 +341,14 @@ function tryParseExportStatement () {
339341

340342
let ch = commentWhitespace(true);
341343

344+
// Only commit the statement start once this is a real export: skipExpression
345+
// re-enters here for an `export`-prefixed identifier (e.g. `exports`) in an
346+
// initializer, which would otherwise clobber the start for later bindings.
342347
if (pos === curPos && !isPunctuator(ch))
343348
return;
344349

350+
exportStatementStart = sStartPos;
351+
345352
switch (ch) {
346353
// export default ...
347354
case 100/*d*/:

‎src/lexer.asm.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ export function parse (_source, _name = '@') {
6363
imports.push({ t, n, s, e, ss, se, d, a, at: at.length > 0 ? at : null });
6464
}
6565
while (asm.re()) {
66-
const s = asm.es(), e = asm.ee(), ls = asm.els(), le = asm.ele();
66+
const s = asm.es(), e = asm.ee(), ls = asm.els(), le = asm.ele(), ss = asm.ess();
6767
const n = decodeIfQuoted(s, e);
6868
const ln = ls < 0 ? undefined : decodeIfQuoted(ls, le);
6969
exports.push({
70-
s, e, ls, le,
70+
s, e, ls, le, ss,
7171
n, ln,
7272
});
7373
}

‎src/lexer.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,9 +568,14 @@ void tryParseExportStatement () {
568568

569569
char16_t ch = commentWhitespace(true);
570570

571+
// Only commit the statement start once this is a real export: skipExpression
572+
// re-enters here for an `export`-prefixed identifier (e.g. `exports`) in an
573+
// initializer, which would otherwise clobber the start for later bindings.
571574
if (pos == curPos && !isPunctuator(ch))
572575
return;
573576

577+
export_statement_start = sStartPos;
578+
574579
if (ch == '{') {
575580
pos++;
576581
ch = commentWhitespace(true);

‎src/lexer.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct Export {
7070
const char16_t* end;
7171
const char16_t* local_start;
7272
const char16_t* local_end;
73+
const char16_t* statement_start;
7374
struct Export* next;
7475
};
7576
typedef struct Export Export;
@@ -81,6 +82,7 @@ Export* export_read_head = NULL;
8182
Import* import_write_head = NULL;
8283
Import* import_write_head_last = NULL;
8384
Export* export_write_head = NULL;
85+
const char16_t* export_statement_start = NULL;
8486
void* analysis_base;
8587
void* analysis_head;
8688

@@ -167,6 +169,7 @@ void addExport (const char16_t* start, const char16_t* end, const char16_t* loca
167169
export->end = end;
168170
export->local_start = local_start;
169171
export->local_end = local_end;
172+
export->statement_start = export_statement_start;
170173
export->next = NULL;
171174
hasModuleSyntax = true;
172175
}
@@ -229,6 +232,10 @@ int32_t els () {
229232
int32_t ele () {
230233
return export_read_head->local_end ? export_read_head->local_end - source : -1;
231234
}
235+
// getExportStatementStart
236+
uint32_t ess () {
237+
return export_read_head->statement_start - source;
238+
}
232239
// readImport
233240
bool ri () {
234241
if (import_read_head == NULL)

‎src/lexer.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ export interface ExportSpecifier {
195195
* End of local name, or -1.
196196
*/
197197
readonly le: number;
198+
199+
/**
200+
* Start of the export statement.
201+
*
202+
* Only the statement start is provided; the statement end is not tracked
203+
* (see https://github.com/guybedford/es-module-lexer/issues/112). Every
204+
* specifier of a statement reports the same start, so `export { a, b }`
205+
* returns the same `ss` for both `a` and `b`.
206+
*
207+
* @example
208+
* const source = `export { a, b } from 'mod'`;
209+
* const [imports, exports] = parse(source);
210+
* source.slice(exports[0].ss, exports[0].ss + 6);
211+
* // Returns "export"
212+
*/
213+
readonly ss: number;
198214
}
199215

200216
export interface ParseError extends Error {
@@ -250,11 +266,11 @@ export function parse (source: string, name = '@'): readonly [
250266
imports.push({ n, t, s, e, ss, se, d, a, at: at.length > 0 ? at : null });
251267
}
252268
while (wasm.re()) {
253-
const s = wasm.es(), e = wasm.ee(), ls = wasm.els(), le = wasm.ele();
269+
const s = wasm.es(), e = wasm.ee(), ls = wasm.els(), le = wasm.ele(), ss = wasm.ess();
254270
const n = decodeIfQuoted(source.slice(s, e));
255271
const ln = ls < 0 ? undefined : decodeIfQuoted(source.slice(ls, le));
256272
exports.push({
257-
s, e, ls, le,
273+
s, e, ls, le, ss,
258274
n, ln,
259275
});
260276
}
@@ -311,6 +327,8 @@ let wasm: {
311327
els(): number;
312328
/** getExportStart */
313329
es(): number;
330+
/** getExportStatementStart */
331+
ess(): number;
314332
/** facade */
315333
f(): boolean;
316334
/** hasModuleSyntax */

‎test/_unit.cjs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,37 @@ function x() {
12581258
assertExportIs(source, exports[1], { n: 'yy', ln: undefined });
12591259
});
12601260

1261+
test('Export statement start', () => {
1262+
const source = [
1263+
`export const x = 1;`,
1264+
`export function fn () {}`,
1265+
`export class C {}`,
1266+
`export default 42;`,
1267+
`export { c, d };`,
1268+
`export * as ns from './mod';`,
1269+
`export { e, f as g } from './re';`
1270+
].join('\n');
1271+
const [, exports] = parse(source);
1272+
assert.strictEqual(exports.length, 9);
1273+
1274+
for (const expt of exports)
1275+
assert.strictEqual(source.slice(expt.ss, expt.ss + 6), 'export');
1276+
1277+
// Each specifier reports its statement's start, so the `{ c, d }` names and
1278+
// the re-exported `e` / `g` each share one `ss`.
1279+
assert.strictEqual(exports[0].ss, 0);
1280+
assert.strictEqual(exports[4].ss, exports[5].ss);
1281+
assert.strictEqual(exports[4].ss, source.indexOf('export { c, d }'));
1282+
assert.strictEqual(exports[7].ss, exports[8].ss);
1283+
assert.strictEqual(exports[7].ss, source.indexOf('export { e, f as g }'));
1284+
1285+
// Distinct statements resolve to their own offset, not a shared constant.
1286+
assert.strictEqual(exports[1].ss, source.indexOf('export function'));
1287+
assert.strictEqual(exports[2].ss, source.indexOf('export class'));
1288+
assert.strictEqual(exports[3].ss, source.indexOf('export default'));
1289+
assert.strictEqual(exports[6].ss, source.indexOf('export * as'));
1290+
});
1291+
12611292
suite('Import From', () => {
12621293
if (!js)
12631294
test('non-identifier-string as (doubleQuote)', () => {

0 commit comments

Comments
 (0)