diff --git a/feature/asi.js b/feature/asi.js index 299b58b..26329d1 100644 --- a/feature/asi.js +++ b/feature/asi.js @@ -27,10 +27,16 @@ const MAX_ASI_DEPTH = 100; parse.asi = (a, p, expr, b, items) => { if (p >= lvl || asiDepth >= MAX_ASI_DEPTH) return; + // Bail if the inner expr didn't actually consume anything. Without this, a + // lookup handler that returns a non-array sentinel (e.g. switch.js's + // `reserve` flagging `case`/`default` inside a switch body) lets expr return + // a truthy token without advancing idx, and the outer ASI loop appends to its + // semicolon-list forever. + const beforeIdx = idx; asiDepth++; try { b = expr(lvl - .5); } finally { asiDepth--; } - if (!b) return; + if (!b || idx === beforeIdx) return; items = b?.[0] === ';' ? b.slice(1) : [b]; return a?.[0] === ';' ? (a.push(...items), a) : [';', a, ...items]; }; diff --git a/test/feature/control.js b/test/feature/control.js index f871a39..aac9997 100644 --- a/test/feature/control.js +++ b/test/feature/control.js @@ -155,6 +155,21 @@ test('control: switch basic', t => { is(ast[3][0], 'case') }) +// ASI inside a switch body: when an inner expr() call hits a reserved +// keyword that signals "matched but did not consume" (e.g. case/default +// while inSwitch), the ASI loop must stop. Without the no-progress guard, +// the recursion appended to its semicolon-list until the array exceeded +// its max length and threw `Invalid array length`. +test('control: switch with multi-statement case bodies parses without ASI loop', t => { + is(parse(`switch (x) { + case 1: + a + b + case 2: + c + }`)[0], 'switch') +}) + test('control: switch compile', t => { // Basic case match let ctx = { x: 1, y: 0 }