Skip to content

Commit

Permalink
Support super in eval() [fix]
Browse files Browse the repository at this point in the history
Fixes #308.
  • Loading branch information
overlookmotel committed Nov 16, 2023
1 parent 39e317c commit 0c308b8
Show file tree
Hide file tree
Showing 5 changed files with 621 additions and 21 deletions.
21 changes: 13 additions & 8 deletions lib/init/eval.js
Expand Up @@ -100,9 +100,11 @@ function evalIndirect(code, tracker, filename, blockIdCounter, externalPrefixNum
* @returns {*} - Result of `eval()` call
*/
function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirectLocal) {
const callArgs = args.slice(0, -5),
const callArgs = args.slice(0, -6),
code = callArgs[0],
[possibleEval, execEvalSingleArg, execEvalSpread, scopeDefs, isStrict] = args.slice(-5);
[
possibleEval, execEvalSingleArg, execEvalSpread, scopeDefs, isStrict, superIsProto
] = args.slice(-6);

// If var `eval` where `eval()` is called is not global `eval`,
// call `eval()` with original args unaltered
Expand All @@ -116,7 +118,8 @@ function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirec
const state = {
currentBlock: undefined,
currentThisBlock: undefined,
currentSuperBlock: undefined
currentSuperBlock: undefined,
currentSuperIsProto: false
};
const tempVars = [];
let allowNewTarget = false;
Expand All @@ -132,11 +135,12 @@ function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirec
} else if (varName === 'new.target') {
createNewTargetBinding(block);
allowNewTarget = true;
} else if (varName === 'super') {
// Don't create binding - `super` binding is created lazily
state.currentSuperBlock = block;
state.currentSuperIsProto = superIsProto;
tempVars.push({value: tempVarValue, block, varName});
} else {
// TODO: Also need to set `superIsProto` and create a new temp var for `super` target
// (the external var could be shadowed inside `eval()` if prefix num is changing)
if (varName === 'super') state.currentSuperBlock = block;

// Whether var is function is not relevant because it will always be in external scope
// of the functions it's being recorded on, and value of `isFunction` only has any effect
// for internal vars
Expand Down Expand Up @@ -231,10 +235,11 @@ function compile(
// Parse code.
// If parsing fails, swallow error. Expression will be passed to `eval()`
// which should throw - this maintains native errors.
const allowSuper = !!state?.currentSuperBlock;
let ast;
try {
ast = parseImpl(
code, filename, false, false, allowNewTarget, false, isStrict, false, undefined
code, filename, false, false, allowNewTarget, allowSuper, false, isStrict, false, undefined
).ast;
} catch (err) {
return {code, shouldThrow: true, internalPrefixNum: externalPrefixNum, tempVars};
Expand Down
2 changes: 1 addition & 1 deletion lib/instrument/index.js
Expand Up @@ -39,7 +39,7 @@ function parse(code, options) {
assert(!sourceMaps || filename, 'options.filename must be provided when source maps enabled');

return parseImpl(
code, filename, isEsm, isCommonJs, isCommonJs, isJsx, isStrict, sourceMaps, inputSourceMap
code, filename, isEsm, isCommonJs, isCommonJs, false, isJsx, isStrict, sourceMaps, inputSourceMap
);
}

Expand Down
8 changes: 6 additions & 2 deletions lib/instrument/instrument.js
Expand Up @@ -33,6 +33,8 @@ const babelOptionsCache = {};
* @param {boolean} isCommonJs - `true` if is CommonJS
* @param {boolean} allowNewTarget - `true` if `new.target` can be used outside a function
* (CommmonJS or direct `eval()` within a function)
* @param {boolean} allowSuper - `true` if `super` can be used outside a function
* (direct `eval()` within a method)
* @param {boolean} isJsx - `true` if source contains JSX syntax
* @param {boolean} isStrict - `true` if strict mode
* @param {boolean} sourceMaps - `true` if source maps enabled
Expand All @@ -44,14 +46,16 @@ const babelOptionsCache = {};
* @throws {Error} - If parsing error
*/
function parseImpl(
code, filename, isEsm, isCommonJs, allowNewTarget, isJsx, isStrict, sourceMaps, inputSourceMap
code, filename, isEsm, isCommonJs, allowNewTarget, allowSuper, isJsx, isStrict,
sourceMaps, inputSourceMap
) {
// Parse code to AST
const ast = parse(code, {
sourceType: isEsm ? 'module' : 'script',
strictMode: isStrict,
allowReturnOutsideFunction: isCommonJs,
allowNewTargetOutsideFunction: allowNewTarget,
allowSuperOutsideMethod: allowSuper,
plugins: ['v8intrinsic', ...(isJsx ? ['jsx'] : [])]
});

Expand Down Expand Up @@ -97,7 +101,7 @@ function instrumentCodeImpl(
) {
// Parse code to AST
let {ast, sources} = parseImpl( // eslint-disable-line prefer-const
code, filename, isEsm, isCommonJs, isCommonJs, isJsx, isStrict, sourceMaps, inputSourceMap
code, filename, isEsm, isCommonJs, isCommonJs, false, isJsx, isStrict, sourceMaps, inputSourceMap
);

// Instrument AST
Expand Down
27 changes: 17 additions & 10 deletions lib/instrument/visitors/eval.js
Expand Up @@ -52,7 +52,7 @@ function visitEval(node, parent, key, state) {
// Queue instrumentation on 2nd pass
state.secondPass(
instrumentEval, node, isEvalCall, parent, key, state.currentBlock, fn,
state.isStrict, canUseSuper, state
state.isStrict, canUseSuper, state.currentSuperIsProto, state
);
}

Expand All @@ -66,16 +66,19 @@ function visitEval(node, parent, key, state) {
* @param {Object} [fn] - Function object for function `eval` is in (`undefined` if not in a function)
* @param {boolean} isStrict - `true` if is strict mode
* @param {boolean} canUseSuper - `true` if `super()` can be used in `eval()`
* @param {boolean} superIsProto - `true` if `super` refers to class prototype
* @param {Object} state - State object
* @returns {undefined}
*/
function instrumentEval(node, isEvalCall, parent, key, block, fn, isStrict, canUseSuper, state) {
function instrumentEval(
node, isEvalCall, parent, key, block, fn, isStrict, canUseSuper, superIsProto, state
) {
// Check `eval` is global
if (!isGlobalEval(block)) return;

// Handle either `eval()` call or `eval` identifier
if (isEvalCall) {
instrumentEvalCall(parent, block, fn, isStrict, canUseSuper, state);
instrumentEvalCall(parent, block, fn, isStrict, canUseSuper, superIsProto, state);
} else {
instrumentEvalIdentifier(node, parent, key, state);
}
Expand All @@ -93,10 +96,11 @@ function instrumentEval(node, isEvalCall, parent, key, block, fn, isStrict, canU
* @param {Object} [fn] - Function object for function `eval` is in (`undefined` if not in a function)
* @param {boolean} isStrict - `true` if is strict mode
* @param {boolean} canUseSuper - `true` if `super()` can be used in `eval()`
* @param {boolean} superIsProto - `true` if `super` refers to class prototype
* @param {Object} state - State object
* @returns {undefined}
*/
function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) {
function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, superIsProto, state) {
// If no arguments, leave as is
const argNodes = callNode.arguments;
if (argNodes.length === 0) return;
Expand All @@ -116,7 +120,7 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) {
const varDefsNodes = [];
for (const [varName, binding] of Object.entries(block.bindings)) {
if (varNamesUsed.has(varName)) continue;
if (isStrict && varName !== 'this' && isReservedWord(varName)) continue;
if (isStrict && varName !== 'this' && varName !== 'super' && isReservedWord(varName)) continue;

// Ignore `require` as it would render the function containing `eval()` un-serializable.
// Also ignore CommonJS wrapper function's `arguments` as that contains `require` too.
Expand All @@ -138,12 +142,16 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) {
while (varDefNodes.length !== 3) varDefNodes.push(null);
varDefNodes.push(t.arrayExpression(binding.argNames.map(argName => t.stringLiteral(argName))));
}
if (varName === 'super') {
while (varDefNodes.length !== 4) varDefNodes.push(null);
varDefNodes.push(binding.varNode);
}
varDefsNodes.push(t.arrayExpression(varDefNodes));

// If var is external to function, record function as using this var.
// Ignore `new.target` as it's not possible to recreate.
// Ignore `new.target` and `super` as they're not possible to recreate.
// https://github.com/overlookmotel/livepack/issues/448
if (blockIsExternalToFunction && varName !== 'new.target') {
if (blockIsExternalToFunction && varName !== 'new.target' && varName !== 'super') {
activateBinding(binding, varName);
const externalVar = getOrCreateExternalVar(externalVars, block, varName, binding);
externalVar.isReadFrom = true;
Expand Down Expand Up @@ -186,7 +194,8 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, state) {
[tempVarNode], t.callExpression(t.identifier('eval'), [t.spreadElement(tempVarNode)])
),
t.arrayExpression(scopeNodes.reverse()),
t.booleanLiteral(isStrict)
t.booleanLiteral(isStrict),
t.booleanLiteral(canUseSuper && superIsProto)
);
}

Expand Down Expand Up @@ -254,8 +263,6 @@ function activateSuperIfIsUsable(fn, state) {
setSuperIsProtoOnFunctions(superBlock, fn, state);
}

// TODO: Also need to pass `superIsProto` and `superVarNode` to `evalDirect()`
// for it to set inside `eval()`
activateSuperBinding(superBlock, state);
return true;
}

0 comments on commit 0c308b8

Please sign in to comment.