From f726800e1c60d17893e7d3b54fec244e6747a8df Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Thu, 23 Nov 2023 00:02:06 +0000 Subject: [PATCH] Allow freezing individual external vars (prep for `with ()` support) --- lib/instrument/visitors/eval.js | 21 +++++++++++++-------- lib/instrument/visitors/function.js | 1 + lib/serialize/blocks.js | 10 ++++++---- lib/serialize/functions.js | 9 +++++---- lib/serialize/parseFunction.js | 14 +++++++++++--- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/instrument/visitors/eval.js b/lib/instrument/visitors/eval.js index 55d3d5c2..72e62437 100644 --- a/lib/instrument/visitors/eval.js +++ b/lib/instrument/visitors/eval.js @@ -165,14 +165,19 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, superIsP // If var is external to function, record function as using this var. // Ignore `new.target` and `super` as they're not possible to recreate. // https://github.com/overlookmotel/livepack/issues/448 - if (externalVars && varName !== 'new.target' && varName !== 'super') { - activateBinding(binding, varName); - const externalVar = getOrCreateExternalVar(externalVars, block, varName, binding); - externalVar.isReadFrom = true; - if (!isConst) externalVar.isAssignedTo = true; - if (!externalVar.isFrozenName) { - externalVar.isFrozenName = true; - externalVar.trails.length = 0; + if (externalVars) { + if (varName === 'new.target' || varName === 'super') { + const externalVar = externalVars.get(block)?.[varName]; + if (externalVar) externalVar.isFrozenName = true; + } else { + activateBinding(binding, varName); + const externalVar = getOrCreateExternalVar(externalVars, block, varName, binding); + externalVar.isReadFrom = true; + if (!isConst) externalVar.isAssignedTo = true; + if (!externalVar.isFrozenName) { + externalVar.isFrozenName = true; + externalVar.trails.length = 0; + } } } } diff --git a/lib/instrument/visitors/function.js b/lib/instrument/visitors/function.js index 6885f766..38c6fa5f 100644 --- a/lib/instrument/visitors/function.js +++ b/lib/instrument/visitors/function.js @@ -762,6 +762,7 @@ function createFunctionInfoFunction(fn, state) { return { isReadFrom: varProps.isReadFrom || undefined, isAssignedTo: varProps.isAssignedTo || undefined, + isFrozenName: varProps.isFrozenName || undefined, isFrozenInternalName: varProps.binding.isFrozenName || undefined, trails: varProps.trails }; diff --git a/lib/serialize/blocks.js b/lib/serialize/blocks.js index 4ea9fe97..483d559d 100644 --- a/lib/serialize/blocks.js +++ b/lib/serialize/blocks.js @@ -62,9 +62,11 @@ module.exports = { const {containsEval} = block, paramsByName = Object.create(null); let frozenNamesIsCloned = false; - const params = [...block.params.keys()].map((name) => { + const params = [...block.params].map(([name, {isFrozenName}]) => { + if (containsEval) isFrozenName = true; const param = { name, + isFrozenName, // Identifier nodes referring to this param localVarNodes: [], // If param always contains another function defined in this block, @@ -83,7 +85,7 @@ module.exports = { paramsByName[name] = param; // `super` and `new.target` are not actually frozen - if (containsEval && !['super', 'new.target'].includes(name)) { + if (isFrozenName && !['super', 'new.target'].includes(name)) { if (!frozenNamesIsCloned) { frozenNames = new Set(frozenNames); frozenNamesIsCloned = true; @@ -637,7 +639,7 @@ module.exports = { if (paramName === 'new.target') { // `new.target` is always renamed as it can't be provided for `eval()` anyway - newName = transformVarName('newTarget', containsEval); + newName = transformVarName('newTarget', param.isFrozenName); // Convert `MetaProperty` nodes into `Identifier`s with new name for (const varNode of param.localVarNodes) { @@ -665,7 +667,7 @@ module.exports = { } else { // Frozen var name (potentially used in `eval()`) // eslint-disable-next-line no-lonely-if - if (paramName === 'this' || (paramName === 'arguments' && paramsByName.this)) { + if (paramName === 'this' || (paramName === 'arguments' && paramsByName.this?.isFrozenName)) { // `this` or `arguments` captured from function. // `arguments` is only injected with a function wrapper if `this` is frozen too. // Otherwise, can just make `arguments` a normal param. diff --git a/lib/serialize/functions.js b/lib/serialize/functions.js index 99b95ba9..2593b3ef 100644 --- a/lib/serialize/functions.js +++ b/lib/serialize/functions.js @@ -150,12 +150,13 @@ module.exports = { } // Add var names to block - for (const [varName, {isAssignedTo}] of Object.entries(vars)) { + for (const [varName, {isAssignedTo, isFrozenName}] of Object.entries(vars)) { const param = params.get(varName); if (!param) { - params.set(varName, {isMutable: isAssignedTo || argNames?.has(varName)}); - } else if (isAssignedTo) { - param.isMutable = true; + params.set(varName, {isMutable: isAssignedTo || argNames?.has(varName), isFrozenName}); + } else { + if (isAssignedTo) param.isMutable = true; + if (isFrozenName) param.isFrozenName = true; } } diff --git a/lib/serialize/parseFunction.js b/lib/serialize/parseFunction.js index 6acc0a50..0f0a7fcd 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -82,7 +82,11 @@ module.exports = function parseFunction( blockName, vars: mapValues(vars, (varProps, varName) => { externalVars[varName] = []; - return {isReadFrom: !!varProps.isReadFrom, isAssignedTo: !!varProps.isAssignedTo}; + return { + isReadFrom: !!varProps.isReadFrom, + isAssignedTo: !!varProps.isAssignedTo, + isFrozenName: !!varProps.isFrozenName + }; }) }); } @@ -867,14 +871,18 @@ function resolveFunctionInfo( const {blockId} = scope; if (blockId < fnId) { // External var - for (const [varName, {isReadFrom, isAssignedTo, trails}] of Object.entries(scope.vars)) { + for ( + const [varName, {isReadFrom, isAssignedTo, isFrozenName, trails}] + of Object.entries(scope.vars) + ) { if (isNestedFunction) { const scopeDefVar = scopeDefs.get(blockId).vars[varName]; if (isReadFrom) scopeDefVar.isReadFrom = true; if (isAssignedTo) scopeDefVar.isAssignedTo = true; + if (isFrozenName) scopeDefVar.isFrozenName = true; } - externalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); + if (!isFrozenName) externalVars[varName].push(...trailsToNodes(fnNode, trails, varName)); } } else { // Var which is external to current function, but internal to function being serialized