mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 09:55:29 +02:00
fix(browse): chained IIFE + second statement no longer misclassified as one expression
isSingleParenOrIifeExpression accepted any tail after the initial group's
close as long as trailing chars looked chain-ish, so
`(async()=>{await 1})().then(x=>x); console.log('done')` classified as a
single expression and the expression wrapper emitted a SyntaxError. The
tail is now consumed as a strict member/call/index/optional-chain walk to
END of input via a shared string/escape-aware findBalancedClose scanner;
anything else (';', operators) demotes to the block wrapper. Negative +
positive tests added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
159b48e813
commit
ce3f3848a7
+52
-46
@@ -29,23 +29,17 @@ export function hasAwait(code: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect whether code is a single top-level expression (such as an IIFE or parenthesized expression),
|
* Find the index of the bracket that closes the group opened at `start`
|
||||||
* even if it contains internal statements, semicolons, or multiple lines (#2727).
|
* (which must be `(` or `[`), skipping string literals and escapes.
|
||||||
|
* Returns -1 when unbalanced. Shared by the single-expression scanner below.
|
||||||
*/
|
*/
|
||||||
export function isSingleParenOrIifeExpression(code: string): boolean {
|
function findBalancedClose(src: string, start: number): number {
|
||||||
const trimmed = code.trim().replace(/;+\s*$/, '');
|
const open = src[start];
|
||||||
let src = trimmed;
|
const close = open === '(' ? ')' : ']';
|
||||||
if (src.startsWith('await ') || src.startsWith('await\t') || src.startsWith('await\n')) {
|
|
||||||
src = src.slice(5).trim();
|
|
||||||
}
|
|
||||||
if (!src.startsWith('(')) return false;
|
|
||||||
|
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
let inString: string | null = null;
|
let inString: string | null = null;
|
||||||
let escape = false;
|
let escape = false;
|
||||||
let mainCloseIndex = -1;
|
for (let i = start; i < src.length; i++) {
|
||||||
|
|
||||||
for (let i = 0; i < src.length; i++) {
|
|
||||||
const char = src[i];
|
const char = src[i];
|
||||||
if (escape) {
|
if (escape) {
|
||||||
escape = false;
|
escape = false;
|
||||||
@@ -63,49 +57,61 @@ export function isSingleParenOrIifeExpression(code: string): boolean {
|
|||||||
inString = char;
|
inString = char;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (char === '(') {
|
if (char === open) {
|
||||||
depth++;
|
depth++;
|
||||||
} else if (char === ')') {
|
} else if (char === close) {
|
||||||
depth--;
|
depth--;
|
||||||
if (depth === 0) {
|
if (depth === 0) return i;
|
||||||
mainCloseIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether code is a single top-level expression (such as an IIFE or parenthesized expression),
|
||||||
|
* even if it contains internal statements, semicolons, or multiple lines (#2727).
|
||||||
|
*
|
||||||
|
* The tail after the initial `(...)` group must be a CONTINUOUS member/call/
|
||||||
|
* index chain consumed to end-of-input. Accepting any tail that merely starts
|
||||||
|
* with `(` or `.` classified `(iife)().then(x=>x); stmt` as a single
|
||||||
|
* expression, and the expression wrapper then emitted a SyntaxError.
|
||||||
|
*/
|
||||||
|
export function isSingleParenOrIifeExpression(code: string): boolean {
|
||||||
|
const trimmed = code.trim().replace(/;+\s*$/, '');
|
||||||
|
let src = trimmed;
|
||||||
|
if (src.startsWith('await ') || src.startsWith('await\t') || src.startsWith('await\n')) {
|
||||||
|
src = src.slice(5).trim();
|
||||||
|
}
|
||||||
|
if (!src.startsWith('(')) return false;
|
||||||
|
|
||||||
|
const mainCloseIndex = findBalancedClose(src, 0);
|
||||||
if (mainCloseIndex === -1) return false;
|
if (mainCloseIndex === -1) return false;
|
||||||
|
|
||||||
const rest = src.slice(mainCloseIndex + 1).trim();
|
// Consume the ENTIRE tail as a chain of `.member`, `(...)`, `[...]`, or
|
||||||
if (rest === '') return true;
|
// optional-chaining segments. Anything else (a `;`, a second statement,
|
||||||
|
// an operator) means this is not a single expression.
|
||||||
if (rest.startsWith('(')) {
|
let i = mainCloseIndex + 1;
|
||||||
let callDepth = 0;
|
while (i < src.length) {
|
||||||
let callCloseIndex = -1;
|
const ch = src[i];
|
||||||
let inStr: string | null = null;
|
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
||||||
let esc = false;
|
i++;
|
||||||
for (let i = 0; i < rest.length; i++) {
|
continue;
|
||||||
const c = rest[i];
|
|
||||||
if (esc) { esc = false; continue; }
|
|
||||||
if (c === '\\' && inStr) { esc = true; continue; }
|
|
||||||
if (inStr) { if (c === inStr) inStr = null; continue; }
|
|
||||||
if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
|
|
||||||
if (c === '(') callDepth++;
|
|
||||||
else if (c === ')') {
|
|
||||||
callDepth--;
|
|
||||||
if (callDepth === 0) {
|
|
||||||
callCloseIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (callCloseIndex !== -1) {
|
if (ch === '(' || ch === '[') {
|
||||||
const afterCall = rest.slice(callCloseIndex + 1).trim();
|
const close = findBalancedClose(src, i);
|
||||||
if (afterCall === '' || afterCall.startsWith('.')) return true;
|
if (close === -1) return false;
|
||||||
|
i = close + 1;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ch === '.' || (ch === '?' && src[i + 1] === '.')) {
|
||||||
|
i += ch === '.' ? 1 : 2;
|
||||||
|
// Member name (or the `(`/`[` of `?.()` / `?.[]`, handled next loop).
|
||||||
|
while (i < src.length && /[\w$]/.test(src[i])) i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detect whether code needs a block wrapper {…} vs expression wrapper (…) inside an async IIFE. */
|
/** Detect whether code needs a block wrapper {…} vs expression wrapper (…) inside an async IIFE. */
|
||||||
|
|||||||
@@ -6,6 +6,28 @@ import {
|
|||||||
wrapForEvaluate,
|
wrapForEvaluate,
|
||||||
} from '../src/read-commands';
|
} from '../src/read-commands';
|
||||||
|
|
||||||
|
// Regression: a chained IIFE followed by a SECOND statement was classified as
|
||||||
|
// a single expression, and the expression wrapper emitted a SyntaxError. The
|
||||||
|
// tail after the initial group must be a continuous member/call/index chain
|
||||||
|
// to END of input.
|
||||||
|
describe('chained IIFE followed by a second statement', () => {
|
||||||
|
it('is NOT a single expression and wraps to syntactically valid code', () => {
|
||||||
|
const code = "(async()=>{await 1})().then(x=>x); console.log('done')";
|
||||||
|
expect(isSingleParenOrIifeExpression(code)).toBe(false);
|
||||||
|
const wrapped = wrapForEvaluate(code);
|
||||||
|
expect(() => new Function('return ' + wrapped)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still accepts a pure chained IIFE (with optional chaining and index access)', () => {
|
||||||
|
expect(isSingleParenOrIifeExpression('(async()=>{await 1})().then(x=>x)')).toBe(true);
|
||||||
|
expect(isSingleParenOrIifeExpression('(getObj())?.items[0].run()')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an operator tail', () => {
|
||||||
|
expect(isSingleParenOrIifeExpression('(a)() + 1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('browse js / eval wrapping (#2727)', () => {
|
describe('browse js / eval wrapping (#2727)', () => {
|
||||||
it('detects presence of await keyword', () => {
|
it('detects presence of await keyword', () => {
|
||||||
expect(hasAwait('await Promise.resolve(1)')).toBe(true);
|
expect(hasAwait('await Promise.resolve(1)')).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user