diff --git a/README.md b/README.md index 00282bcc..60820035 100644 --- a/README.md +++ b/README.md @@ -570,8 +570,6 @@ NB Applications can *use* any of these within functions, just that instances of * Unsupported: `export default Promise.resolve();` (Promise instance serialized directly) * Unsupported: `const p = Promise.resolve(); export default function f() { return p; };` (Promise instance in outer scope of exported function) -`with (...) {...}` is also not supported where it alters the scope of a function being serialized. - ### Browser code This works in part. You can, for example, build a simple React app with Livepack. diff --git a/lib/init/eval.js b/lib/init/eval.js index ee1e9a0c..afa9413f 100644 --- a/lib/init/eval.js +++ b/lib/init/eval.js @@ -119,7 +119,8 @@ function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirec currentBlock: undefined, currentThisBlock: undefined, currentSuperBlock: undefined, - currentSuperIsProto: false + currentSuperIsProto: false, + currentWithBlock: undefined }; const tempVars = []; let allowNewTarget = false; @@ -147,6 +148,11 @@ function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirec createBindingWithoutNameCheck( block, varName, {isConst: !!isConst, isSilentConst: !!isSilentConst, argNames} ); + + if (varName === 'with') { + state.currentWithBlock ||= block; + tempVars.push({value: tempVarValue, block, varName}); + } } } } diff --git a/lib/init/tracker.js b/lib/init/tracker.js index e2942da7..55758b21 100644 --- a/lib/init/tracker.js +++ b/lib/init/tracker.js @@ -10,7 +10,8 @@ module.exports = createTracker; // Imports -const addEvalFunctionsToTracker = require('./eval.js'), +const addWrapWithFunctionToTracker = require('./with.js'), + addEvalFunctionsToTracker = require('./eval.js'), {tracker} = require('../shared/tracker.js'); // Exports @@ -24,6 +25,7 @@ const addEvalFunctionsToTracker = require('./eval.js'), */ function createTracker(filename, blockIdCounter, prefixNum) { const localTracker = (getFnInfo, getScopes) => tracker(getFnInfo, getScopes); + addWrapWithFunctionToTracker(localTracker, prefixNum); addEvalFunctionsToTracker(localTracker, filename, blockIdCounter, prefixNum); return localTracker; } diff --git a/lib/init/with.js b/lib/init/with.js new file mode 100644 index 00000000..34b6bbec --- /dev/null +++ b/lib/init/with.js @@ -0,0 +1,132 @@ +/* -------------------- + * livepack module + * Functions to handle `with` statements + * ------------------*/ + +/* eslint-disable no-restricted-properties, no-extend-native */ + +'use strict'; + +// Modules +const assert = require('simple-invariant'); + +// Imports +const {withBypassIsEnabled} = require('../shared/with.js'), + {INTERNAL_VAR_NAMES_PREFIX} = require('../shared/constants.js'); + +// Exports + +module.exports = addWrapWithFunctionToTracker; + +const nativeEval = eval; // eslint-disable-line no-eval + +let withState; + +/** + * Add `wrapWith` function to tracker. + * + * `with` statements present 2 problems for instrumentation: + * + * 1. Tracker calls need to get the values of vars outside the `with ()`, + * but `with ()` could block access to them. + * 2. `with ()` can block access to Livepack's internal vars (e.g. `livepack_tracker`), + * causing Livepack's internal mechanisms to malfunction. + * + * In both cases, it's necessary to "smuggle" the values of variables outside `with ()` to inside, + * so that properties of the `with ()` object can't interfere with them. + * + * The only solution which works in all cases is to temporarily write values to a global, + * and then retrieve them inside `with ()`. + * + * To avoid this being visible to user code, repurpose an existing global function + * `Object.prototype.__defineSetter__`. + * `__defineSetter__` is chosen because it's fairly obscure, but could use any method. + * + * `with (obj) x();` is instrumented as: + * + * ``` + * with ( + * livepack_tracker.wrapWith( + * obj, + * (eval, livepack_temp_3) => eval(livepack_temp_3), + * () => eval + * ) + * ) with ( {}.__defineSetter__() ) x(); + * ``` + * + * `wrapWith()` stores the 2 functions in `withState`, and `{}.__defineSetter__()` retrieves them again. + * `__defineSetter__()` returns a Proxy which allows dynamically overriding the original `with ()` + * by using the 2 functions `wrapWith()` was called with to access any var outside the `with ()`. + * This Proxy is used as the value for a 2nd `with ()` which is inserted inside the original. + * + * @param {Function} tracker - Tracker function + * @param {number} prefixNum - Internal vars prefix num + * @returns {undefined} + */ +function addWrapWithFunctionToTracker(tracker, prefixNum) { + tracker.wrapWith = (withValue, runEval, getEval) => { + // Don't do anything if `null` or `undefined`, as it will error + if (withValue == null) return withValue; + + // Throw an error if user has changed value of `Object.prototype.__defineSetter__` + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, '__defineSetter__'); + assert( + // eslint-disable-next-line no-use-before-define + descriptor?.value === shimmedDefineSetter, + 'Livepack shims `Object.prototype.__defineSetter__` to instrument `with` statements.' + + "It has been altered in user code, which prevents Livepack's correct functioning." + ); + + // Store state for shimmed `__defineSetter__` to retrieve + withState = [prefixNum, runEval, getEval]; + + // Return original `with` value + return withValue; + }; +} + +// Shim `Object.prototype.__defineSetter__`. +// NB: This code runs before globals are catalogued. +// Define replacement as a method, as original does not have a `prototype` property. +// 2 function params to maintain original's `.length` property. +const defineSetter = Object.prototype.__defineSetter__; +const shimmedDefineSetter = { + __defineSetter__(_x, _y) { // eslint-disable-line no-unused-vars + // If being used normally, defer to original + // eslint-disable-next-line prefer-rest-params + if (!withState) return defineSetter.apply(this, arguments); + + // Is being used to smuggle values into `with ()`. + // Get state previously stored by `wrapWith()`. + const [prefixNum, runEval, getEval] = withState; + withState = undefined; + + const internalVarsPrefix = `${INTERNAL_VAR_NAMES_PREFIX}${prefixNum || ''}_`; + + // Return Proxy to be used as object in inner `with ()` statement. + // Proxy is transparent unless either: + // 1. Currently getting scope vars in tracker call. + // 2. Var being accessed is one of Livepack's internal vars. + // In these cases, intercept access which would otherwise hit the outer `with ()`, + // and instead use the `runEval()` function to get/set the variable outside `with ()`. + // If var being accessed is called `eval`, use `getEval()` instead. + return new Proxy(Object.create(null), { + has(target, key) { + return withBypassIsEnabled() || key.startsWith(internalVarsPrefix); + }, + get(target, key) { + if (key === Symbol.unscopables) return undefined; + if (key === 'eval') return getEval(); + return runEval(nativeEval, key); + }, + set(target, key, value) { + // Only used for setting internal temp vars, so no need to handle if key is `v` or `eval` + const set = runEval(nativeEval, `v => ${key} = v`); + set(value); + return true; + } + }); + } +}.__defineSetter__; + +Object.prototype.__defineSetter__ = shimmedDefineSetter; diff --git a/lib/instrument/modify.js b/lib/instrument/modify.js index e4adb441..802d51eb 100644 --- a/lib/instrument/modify.js +++ b/lib/instrument/modify.js @@ -74,6 +74,7 @@ function modifyAst(ast, filename, isCommonJs, isStrict, sources, evalState) { currentThisBlock: undefined, currentSuperBlock: undefined, currentHoistBlock: undefined, + currentWithBlock: undefined, fileBlock: undefined, programBlock: undefined, currentFunction: undefined, diff --git a/lib/instrument/visitors/eval.js b/lib/instrument/visitors/eval.js index 4677f95f..f8171012 100644 --- a/lib/instrument/visitors/eval.js +++ b/lib/instrument/visitors/eval.js @@ -13,6 +13,7 @@ const t = require('@babel/types'); // Imports const {activateSuperBinding} = require('./super.js'), + {activateWithBinding} = require('./with.js'), {getOrCreateExternalVar, activateBlock, activateBinding} = require('../blocks.js'), {createTempVarNode} = require('../internalVars.js'), {copyLocAndComments} = require('../utils.js'), @@ -108,32 +109,37 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, superIsP const varDefsNodes = []; for (const [varName, binding] of block.bindings) { - // Freeze binding to prevent var being renamed when used internally in a function. - // All vars in scope need to be frozen, even if not accessible to `eval()` - // because if they're renamed, they would become accessible to `eval()` when they weren't before. - if (!binding.isFrozenName) { - binding.isFrozenName = true; - binding.trails.length = 0; + if (varName === 'with') { + // Variables accessed from within `eval()` may pass through this with block + activateWithBinding(block, state); + } else { + // Freeze binding to prevent var being renamed when used internally in a function. + // All vars in scope need to be frozen, even if not accessible to `eval()` + // because if they're renamed, they would become accessible to `eval()` when they weren't before. + if (!binding.isFrozenName) { + binding.isFrozenName = true; + binding.trails.length = 0; + } + + // Skip var if it's shadowed by var with same name in higher scope + if (varNamesUsed.has(varName)) continue; + + // If `eval()` is strict mode, skip vars which are illegal to use in strict mode (e.g. `package`) + // as `eval()` won't be able to access them + 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. + if ((varName === 'require' || varName === 'arguments') && block === state.fileBlock) continue; + + // Ignore `super` if it's not accessible + if (varName === 'super' && !canUseSuper) continue; + + varNamesUsed.add(varName); + + if (varName === 'eval') evalIsLocal = true; } - // Skip var if it's shadowed by var with same name in higher scope - if (varNamesUsed.has(varName)) continue; - - // If `eval()` is strict mode, skip vars which are illegal to use in strict mode (e.g. `package`) - // as `eval()` won't be able to access them - 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. - if ((varName === 'require' || varName === 'arguments') && block === state.fileBlock) continue; - - // Ignore `super` if it's not accessible - if (varName === 'super' && !canUseSuper) continue; - - varNamesUsed.add(varName); - - if (varName === 'eval') evalIsLocal = true; - // Create array for var `[varName, isConst, isSilentConst, argNames, tempVarValue]`. // NB: Assignment to var called `arguments` or `eval` is illegal in strict mode. // This is relevant as it affects whether var is flagged as mutable or not. @@ -145,7 +151,7 @@ function instrumentEvalCall(callNode, block, fn, isStrict, canUseSuper, superIsP while (varDefNodes.length !== 3) varDefNodes.push(null); varDefNodes.push(t.arrayExpression(binding.argNames.map(argName => t.stringLiteral(argName)))); } - if (varName === 'super') { + if (varName === 'super' || varName === 'with') { while (varDefNodes.length !== 4) varDefNodes.push(null); varDefNodes.push(binding.varNode); } diff --git a/lib/instrument/visitors/identifier.js b/lib/instrument/visitors/identifier.js index ed979344..72b3f229 100644 --- a/lib/instrument/visitors/identifier.js +++ b/lib/instrument/visitors/identifier.js @@ -16,6 +16,7 @@ module.exports = { // Imports const visitEval = require('./eval.js'), + {activateWithBinding} = require('./with.js'), {getOrCreateExternalVar, activateBlock, activateBinding} = require('../blocks.js'), {checkInternalVarNameClash} = require('../internalVars.js'), { @@ -78,7 +79,18 @@ function ThisExpression(node, state) { // Record as external var use in function. Ignore if internal to function. if (block.id < fn.id) { - recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, state); + // Check if a `with` binding in between, because if so, `this` needs to be frozen, to avoid whatever + // var is used to stand in for `this` being blocked by a property of the `with` object. + // No need to activate the `with` binding, as `this` doesn't read from it. + // Actually, `this` only needs to be frozen if the `with` block ends up in the output's blocks tree. + // e.g. `with (o) return () => this` does not require `this` to be frozen. + // But this can't be determined until serialization time, as it could be *another* function + // in same block which causes the `with` block to be used. + // To avoid complicating matters for this edge case, don't worry about it, and always freeze `this`. + // `this` may be frozen unnecessarily, which produces longer output, but it won't be incorrect. + const isBehindWith = !!state.currentWithBlock && state.currentWithBlock.id > block.id; + + recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, isBehindWith, state); } } @@ -99,8 +111,19 @@ function NewTargetExpression(node, state) { // parser will have already rejected the code as syntax error. const block = state.currentThisBlock; if (block.id < fn.id) { + // Check if a `with` binding in between, because if so, `new.target` needs to be frozen, + // to avoid whatever var is used to stand in for `new.target` being blocked by a property + // of the `with` object. + // At present, `new.target` won't actually be frozen, but setting `isFrozenName` will at least + // cause var name to be prefixed with "_". + // No need to activate the `with` binding, as `new.target` doesn't read from it. + // Same as with `this`, `new.target` only actually needs to be frozen if the `with` block ends + // up in the output's blocks tree. But to simplify matters, just freeze it anyway. + const isBehindWith = !!state.currentWithBlock && state.currentWithBlock.id > block.id; + recordExternalVar( - block.bindings.get('new.target'), block, 'new.target', fn, [...state.trail], true, false, state + block.bindings.get('new.target'), block, 'new.target', fn, [...state.trail], + true, false, isBehindWith, state ); } } @@ -122,7 +145,7 @@ function visitIdentifier(node, varName, isReadFrom, isAssignedTo, state) { // Resolve the binding var refers to in 2nd pass state.secondPass( resolveIdentifier, - node, state.currentBlock, varName, fn, [...state.trail], + node, state.currentBlock, state.currentWithBlock, varName, fn, [...state.trail], isReadFrom, isAssignedTo, state.isStrict, state ); } else { @@ -136,6 +159,7 @@ function visitIdentifier(node, varName, isReadFrom, isAssignedTo, state) { * Resolve binding an identifier refers to and record its usage on function it's used within. * @param {Object} node - Identifier AST node (not needed but passed for debug reasons) * @param {Object} block - Block object + * @param {Object} [withBlock] - Block object for first `with ()` block above identifier * @param {string} varName - Variable name * @param {Object} fn - Function object * @param {Array} trail - Trail @@ -145,13 +169,27 @@ function visitIdentifier(node, varName, isReadFrom, isAssignedTo, state) { * @param {Object} state - State object * @returns {undefined} */ -function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssignedTo, isStrict, state) { +function resolveIdentifier( + node, block, withBlock, varName, fn, trail, isReadFrom, isAssignedTo, isStrict, state +) { // Find binding let binding; do { binding = block.bindings.get(varName); } while (!binding && (block = block.parent)); // eslint-disable-line no-cond-assign + // If `with ()` block between identifier and its binding, the `with ()` could block access + // to the binding. Create external vars for `with` blocks in function, and flag the `binding` + // as frozen name, so var isn't renamed in output. + const isBehindWith = !!withBlock && (!block || withBlock.id > block.id); + if (isBehindWith) { + if (binding) { + binding.isFrozenName = true; + binding.trails.length = 0; + } + createExternalVarsForWiths(withBlock, block, fn, state); + } + // Record if global var if (!binding) { // Vars which are local bindings have var name checked for internal var name clashes @@ -194,7 +232,34 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign isAssignedTo = false; } - recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, state); + recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, state); +} + +/** + * Find all `with` blocks above identifier's binding. + * Activate those `with` blocks and create external vars for them in function. + * @param {Object} withBlock - Block object for `with` block + * @param {Object} [bindingBlock] - Block object for variable binding (which will be above `with` block) + * or `undefined` if no binding (global var) + * @param {Object} fn - Function object + * @param {Object} state - State object + * @returns {undefined} + */ +function createExternalVarsForWiths(withBlock, bindingBlock, fn, state) { + while (true) { // eslint-disable-line no-constant-condition + if (withBlock.id < fn.id) { + // `with ()` is external to function - create external var + const binding = activateWithBinding(withBlock, state); + const externalVar = getOrCreateExternalVar(fn, withBlock, 'with', binding); + externalVar.isReadFrom = true; + } + + // Find next `with` block above binding (if any) + do { + withBlock = withBlock.parent; + if (withBlock === bindingBlock) return; + } while (!withBlock.bindings.has('with')); + } } /** @@ -206,15 +271,22 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign * @param {Array} trail - Trail * @param {boolean} isReadFrom - `true` if variable is read from * @param {boolean} isAssignedTo - `true` if variable is assigned to + * @param {boolean} isBehindWith - `true` if variable may be shadowed by a `with () {}` statement * @param {Object} state - State object * @returns {undefined} */ -function recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, state) { +function recordExternalVar( + binding, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, state +) { activateBlock(block, state); activateBinding(binding); const externalVar = getOrCreateExternalVar(fn, block, varName, binding); if (isReadFrom) externalVar.isReadFrom = true; if (isAssignedTo) externalVar.isAssignedTo = true; // `new.target` isn't actually frozen so needs trail recorded + if (isBehindWith && !externalVar.isFrozenName) { + externalVar.isFrozenName = true; + if (varName !== 'new.target') externalVar.trails.length = 0; + } if (!externalVar.isFrozenName || varName === 'new.target') externalVar.trails.push(trail); } diff --git a/lib/instrument/visitors/statement.js b/lib/instrument/visitors/statement.js index 1122f4d4..5d1a9048 100644 --- a/lib/instrument/visitors/statement.js +++ b/lib/instrument/visitors/statement.js @@ -19,6 +19,7 @@ const VariableDeclaration = require('./variableDeclaration.js'), SwitchStatement = require('./switch.js'), TryStatement = require('./try.js'), ThrowStatement = require('./unary.js'), + {WithStatement} = require('./with.js'), {visitKey, visitKeyMaybe} = require('../visit.js'); // Exports @@ -71,13 +72,6 @@ function ReturnStatement(node, state) { visitKeyMaybe(node, 'argument', Expression, state); } -function WithStatement(node, state) { - // TODO: Maintain a state property `currentWithBlock` which can be used in `resolveBinding()` - // to flag functions which access a var which would be affected by `with` - visitKey(node, 'object', Expression, state); - visitKey(node, 'body', Statement, state); -} - function LabeledStatement(node, state) { visitKey(node, 'body', Statement, state); } diff --git a/lib/instrument/visitors/super.js b/lib/instrument/visitors/super.js index 3b2195af..79f1970b 100644 --- a/lib/instrument/visitors/super.js +++ b/lib/instrument/visitors/super.js @@ -43,6 +43,17 @@ function Super(node, state, parent, key) { superExternalVar = getOrCreateExternalVar(fn, superBlock, 'super', superBinding); superExternalVar.isReadFrom = true; + // Check if a `with` binding in between, because if so, `super` needs to be frozen, + // to avoid whatever var is used to stand in for `super` being blocked by a property + // of the `with` object. + // `super` var won't actually be frozen, but setting `isFrozenName` will at least + // cause var name to be prefixed with '_'. + // No need to activate the `with` binding, as `super` doesn't read from it. + // Same as with `this`, `super` only actually needs to be frozen if the `with` block ends + // up in the output's blocks tree. But to simplify matters, just freeze it anyway. + const isBehindWith = !!state.currentWithBlock && state.currentWithBlock.id > superBlock.id; + if (isBehindWith) superExternalVar.isFrozenName = true; + // `super` expressions in arrow functions require `this`. Create external var for `this`. const isSuperCall = key === 'callee' && parent.type === 'CallExpression'; if (!isSuperCall && fnType === 'ArrowFunctionExpression') { @@ -52,6 +63,14 @@ function Super(node, state, parent, key) { fn, thisBlock, 'this', thisBlock.bindings.get('this') ); thisExternalVar.isReadFrom = true; + + // NB: `isBehindWith` is whether there's a `with ()` between `super` and the `super` binding, + // *not* the `this` binding, which is what we need to know here. But it's not possible + // for `with ()` to be between the `this` and `super` bindings, so the two are equivalent. + if (isBehindWith && !thisExternalVar.isFrozenName) { + thisExternalVar.isFrozenName = true; + thisExternalVar.trails.length = 0; + } } // Set `superIsProto` on all functions between this one and the `super` target diff --git a/lib/instrument/visitors/with.js b/lib/instrument/visitors/with.js new file mode 100644 index 00000000..e063634c --- /dev/null +++ b/lib/instrument/visitors/with.js @@ -0,0 +1,118 @@ +/* -------------------- + * livepack module + * Code instrumentation visitor for `with` statements + * ------------------*/ + +'use strict'; + +// Export +module.exports = { + WithStatement, + activateWithBinding +}; + +// Modules +const t = require('@babel/types'); + +// Imports +const Expression = require('./expression.js'), + Statement = require('./statement.js'), + { + createAndEnterBlock, createBindingWithoutNameCheck, activateBlock, createBlockTempVar + } = require('../blocks.js'), + {createTrackerVarNode, createTempVarNode} = require('../internalVars.js'), + {visitKey} = require('../visit.js'); + +// Exports + +/** + * Visitor for `with () {}` statement. + * @param {Object} node - `with ()` statement AST node + * @param {Object} state - State object + * @returns {undefined} + */ +function WithStatement(node, state) { + // Visit object i.e. expression inside `with (...)` + visitKey(node, 'object', Expression, state); + + // Create block for `with` object. + // `isFrozenName: true` because it shouldn't have an an internal var created for it. + const parentBlock = state.currentBlock, + parentWithBlock = state.currentWithBlock; + const block = createAndEnterBlock('with', false, state); + const binding = createBindingWithoutNameCheck( + block, 'with', {isConst: true, isFrozenName: true}, state + ); + state.currentWithBlock = block; + + // Visit body + visitKey(node, 'body', Statement, state); + + // Exit block + state.currentBlock = parentBlock; + state.currentWithBlock = parentWithBlock; + + // Queue action to wrap `with` object + state.secondPass(instrumentWithObj, node, binding, state); +} + +/** + * Instrument `with ()` statement. + * See explanation in `lib/init/with.js` for what this is doing. + * @param {Object} node - `with ()` statement AST node + * @param {Object} binding - Binding object for the `with ()` object + * @param {Object} state - State object + * @returns {undefined} + */ +function instrumentWithObj(node, binding, state) { + // `with (o) foo;` + // -> + // ``` + // with ( + // livepack_tracker.wrapWith( + // livepack_temp2 = o, + // (eval, livepack_temp_3) => eval(livepack_temp_3), + // () => eval + // ) + // ) with ( {}.__defineSetter__() ) foo; + // ``` + const tempVarNode = createTempVarNode(state), + evalNode = t.identifier('eval'); + node.object = t.callExpression( + // `livepack_tracker.wrapWith` + t.memberExpression(createTrackerVarNode(state), t.identifier('wrapWith')), + [ + // `livepack_temp2 = o` or `o` + binding.varNode + ? t.assignmentExpression('=', binding.varNode, node.object) + : node.object, + // `(eval, livepack_temp_3) => eval(livepack_temp_3)` + t.arrowFunctionExpression( + [evalNode, tempVarNode], + t.callExpression(evalNode, [tempVarNode]) + ), + // `() => eval` + t.arrowFunctionExpression([], evalNode) + ] + ); + + // `foo;` -> `with ( {}.__defineSetter__() ) foo;` + node.body = t.withStatement( + t.callExpression(t.memberExpression(t.objectExpression([]), t.identifier('__defineSetter__')), []), + node.body + ); +} + +/** + * Activate `with ()` object binding. + * Create a temp var node to have `with` object assigned to it. + * @param {Object} block - `with` object block object + * @param {Object} state - State object + * @returns {Object} - Binding + */ +function activateWithBinding(block, state) { + activateBlock(block, state); + const binding = block.bindings.get('with'); + if (!binding.varNode) binding.varNode = createBlockTempVar(block, state); + return binding; +} diff --git a/lib/serialize/blocks.js b/lib/serialize/blocks.js index 6b180e77..0eebbb12 100644 --- a/lib/serialize/blocks.js +++ b/lib/serialize/blocks.js @@ -84,8 +84,8 @@ module.exports = { }; paramsByName[name] = param; - // `super` and `new.target` are not actually frozen - if (isFrozenName && !['super', 'new.target'].includes(name)) { + // `super`, `new.target` and `with` are not actually frozen + if (isFrozenName && !['super', 'new.target', 'with'].includes(name)) { if (!frozenNamesIsCloned) { frozenNames = new Set(frozenNames); frozenNamesIsCloned = true; @@ -627,7 +627,7 @@ module.exports = { const paramNodes = [], {mangle} = this.options; let hasArgumentsOrEvalParam = false, - frozenThisVarName, frozenArgumentsVarName; + frozenThisVarName, frozenArgumentsVarName, withVarName; for (const param of params) { const paramName = param.name; let newName; @@ -652,6 +652,13 @@ module.exports = { // Rename injection node renameInjectionVarNode(); + } else if (paramName === 'with') { + // `with` is always renamed + newName = transformVarName(paramName, containsEval); + withVarName = newName; + // It's not possible for the `with` object to be a circular reference. + // But it can have circular properties. + assert(!param.injectionVarNode, 'Cannot handle circular `with ()` value'); } else if (!param.isFrozenName || paramName === 'super') { // Param can be renamed. // NB: `super` param is always renamed. @@ -666,15 +673,17 @@ module.exports = { renameInjectionVarNode(); } } else { - // Frozen var name (potentially used in `eval()`) + // Frozen var name (potentially used in `eval()`, or behind a `with (...)` block) // eslint-disable-next-line no-lonely-if 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. - // This can be the case if `arguments` is a user-defined variable, - // not `arguments` created by a function. - newName = transformVarName(paramName, true); + // This can be the case if `arguments` is behind a `with (...)` block, + // or `arguments` is a user-defined variable, not `arguments` created by a function. + // If block contains a function containing `eval()`, prepend name of temp var with '_' + // as it'll be accessible to `eval()`. If param is frozen by `with ()`, this isn't required. + newName = transformVarName(paramName, containsEval); if (paramName === 'this') { frozenThisVarName = newName; } else { @@ -683,7 +692,7 @@ module.exports = { assert( !param.injectionVarNode, - 'Cannot handle circular `this` or `arguments` where `this` cannot be renamed due to use of `eval()`' + 'Cannot handle circular `this` or `arguments` where `this` cannot be renamed due to use of `eval()` or `with ()`' ); } else { newName = paramName; @@ -703,10 +712,12 @@ module.exports = { // Handle strict/sloppy mode let isStrict; if (!isRoot) { - if (hasArgumentsOrEvalParam) { + if (hasArgumentsOrEvalParam || withVarName) { // If param named `arguments` or `eval`, scope function must be sloppy mode // or it's a syntax error. - // NB: Only way param will be called `arguments` or `eval` is if it's frozen by an `eval()`. + // NB: Only way param will be called `arguments` or `eval` is if it's frozen by an `eval()` + // or behind a `with (...)` block. + // `with (...)` requires sloppy mode too. isStrict = false; } else if (strictFns.length === 0) { // No strict child functions or child blocks. Block is sloppy if any sloppy children, @@ -735,18 +746,18 @@ module.exports = { returnNode = t.sequenceExpression(internalFunctionNodes); } - // If uses frozen `this`, wrap return value in an IIFE to inject actual `this` - // (and `arguments`, if it's used too). - // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` - // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object - // which would cause an error when this function is called. - // A better solution when outputting sloppy mode code would be to just use a var called `arguments`, - // rather than injecting. Of course this isn't possible in ESM. - // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. - // In sloppy mode, literals passed as `this` get boxed. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript - // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. if (frozenThisVarName) { + // Uses frozen `this`. + // Wrap return value in an IIFE to inject actual `this` (and `arguments`, if it's used too). + // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` + // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object + // which would cause an error when this function is called. + // A better solution when outputting sloppy mode code would be to just use a var called + // `arguments`, rather than injecting. Of course this isn't possible in ESM. + // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. + // In sloppy mode, literals passed as `this` get boxed. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript + // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. returnNode = t.callExpression( t.memberExpression( t.functionExpression(null, [], t.blockStatement([t.returnStatement(returnNode)])), @@ -757,6 +768,11 @@ module.exports = { ...(frozenArgumentsVarName ? [t.identifier(frozenArgumentsVarName)] : []) ] ); + } else if (withVarName) { + // Wrap in `{ with (with$0) return ...; }` + returnNode = t.blockStatement([ + t.withStatement(t.identifier(withVarName), t.returnStatement(returnNode)) + ]); } const node = t.arrowFunctionExpression(paramNodes, returnNode); diff --git a/lib/serialize/functions.js b/lib/serialize/functions.js index 4bb2dae2..d188eda0 100644 --- a/lib/serialize/functions.js +++ b/lib/serialize/functions.js @@ -16,6 +16,7 @@ const util = require('util'), // Imports const {activateTracker, getTrackerResult, trackerError} = require('../shared/tracker.js'), + {enableWithBypass, disableWithBypass} = require('../shared/with.js'), specialFunctions = require('../shared/internal.js').functions, { TRACKER_COMMENT_PREFIX, @@ -316,14 +317,17 @@ module.exports = { errorMessage = 'Expected tracker to throw error'; } - // Call `getScopes()` to get scope vars + // Call `getScopes()` to get scope vars. + // Enable bypass for any `with` statements, so they don't interfere with getting values of vars. let scopeVars; if (!errorMessage) { + enableWithBypass(); try { scopeVars = getScopes(); } catch (err) { errorMessage = getErrorMessage(err); } + disableWithBypass(); } assertBug( diff --git a/lib/serialize/parseFunction.js b/lib/serialize/parseFunction.js index 636f10df..8b14a8de 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -47,7 +47,7 @@ const RUNTIME_DIR_PATH = pathJoin(__dirname, '../runtime/'); * {Object} .internalVars - Object keyed by var name, values are arrays of identifier nodes * {Set} .globalVarNames - Set of names of global vars used * {Set} .reservedVarNames - Set of reserved var names - * (function names and vars accesible to `eval()` which are frozen) + * (function names and vars accesible to `eval()` or `with()` which are frozen) * {string} .name - `.name` of created function * {number} .numParams - `.length` of created function * {boolean} .isClass - `true` if is class @@ -712,7 +712,7 @@ function createTempVarNode(name, internalVars) { * @param {Object} internalVars - Map of var name to array of var nodes * @param {Set} globalVarNames - Set of global var names * @param {Set} reservedVarNames - Set of var names which are reserved - * (function names and vars accesible to `eval()` which are frozen) + * (function names and vars accesible to `eval()` or `with()` which are frozen) * @param {Array} amendments - Array of amendments (const violations or `super`) * @returns {undefined} */ diff --git a/lib/shared/with.js b/lib/shared/with.js new file mode 100644 index 00000000..2c772313 --- /dev/null +++ b/lib/shared/with.js @@ -0,0 +1,37 @@ +/* -------------------- + * livepack module + * `with` bypass state + * ------------------*/ + +'use strict'; + +// Exports + +module.exports = {withBypassIsEnabled, enableWithBypass, disableWithBypass}; + +let isBypassEnabled = false; + +/** + * Get whether `with` bypass is currently enabled. + * Used by `livepack_tracker.wrapWith()`. + * @returns {boolean} - `true` if bypass is currently enabled + */ +function withBypassIsEnabled() { + return isBypassEnabled; +} + +/** + * Enable `with` bypass. + * @returns {undefined} + */ +function enableWithBypass() { + isBypassEnabled = true; +} + +/** + * Disable `with` bypass. + * @returns {undefined} + */ +function disableWithBypass() { + isBypassEnabled = false; +} diff --git a/test/with.test.js b/test/with.test.js new file mode 100644 index 00000000..7a93dd96 --- /dev/null +++ b/test/with.test.js @@ -0,0 +1,648 @@ +/* -------------------- + * livepack module + * Tests for `with` + * ------------------*/ + +/* eslint-disable strict, no-with */ + +// Imports + +const support = require('./support/index.js'), + itSerializes = support.itSerializes.withOptions({strictEnv: false}), + {itSerializesEqual} = support; + +// Tests + +const spy = jest.fn; + +describe('with statements', () => { + describe('outside serialized function', () => { + itSerializes('provides scope to function when no outer binding', { + in() { + with ({x: 123}) { + return () => x; // eslint-disable-line no-undef + } + }, + out: '(a=>{with(a)return()=>x})({x:123})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + } + }); + + itSerializes('provides scope to function when outer binding', { + in() { + const x = 456; + with ({x: 123}) { + return () => x; + } + }, + out: '(x=>a=>{with(a)return()=>x})(456)({x:123})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + } + }); + + itSerializes('allows access to outer binding', { + in() { + const x = 456; + with ({}) { + return () => x; + } + }, + out: '(x=>a=>{with(a)return()=>x})(456)({})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(456); + } + }); + + itSerializes('allows access to global', { + in() { + with ({}) { + return () => console; + } + }, + out: '(a=>{with(a)return()=>console})({})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(console); + } + }); + + describe('allows access to `this`', () => { + itSerializes('when `with ()` not included in output', { + in() { + function outer() { + with ({this: 2, a: 3}) { + return () => this; + } + } + return outer.call({x: 1}); + }, + out: '(a=>function(){return()=>this}.call(a))({x:1})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toEqual({x: 1}); + } + }); + + itSerializes('when `with ()` included in output', { + in() { + const y = 2; + function outer() { + with ({this: 3, a: 4}) { + return () => [this, y]; + } + } + return outer.call({x: 1}); + }, + out: ` + (y=>b=>function(){ + return a=>{ + with(a)return()=>[this,y] + } + }.call(b))(2)({x:1})({this:3,a:4}) + `, + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toEqual([{x: 1}, 2]); + } + }); + }); + + describe('allows access to `arguments`', () => { + itSerializes('in sloppy mode function', { + in() { + function outer() { + with ({a: 4}) { + return () => arguments; // eslint-disable-line prefer-rest-params + } + } + return outer(1, 2, 3); + }, + out: '(arguments=>a=>{with(a)return()=>arguments})(function(){return arguments}(1,2,3))({a:4})', + validate(fn) { + expect(fn).toBeFunction(); + const args = fn(); + expect(args).toBeArguments(); + expect([...args]).toEqual([1, 2, 3]); + } + }); + + itSerializes('in strict mode function', { + in() { + function outer() { + with ({a: 4}) { + return () => { + 'use strict'; + + return arguments; // eslint-disable-line prefer-rest-params + }; + } + } + return outer(1, 2, 3); + }, + out: ` + (arguments=>a=>{ + with(a)return()=>{"use strict";return arguments} + })(function(){return arguments}(1,2,3))({a:4}) + `, + validate(fn) { + expect(fn).toBeFunction(); + const args = fn(); + expect(args).toBeArguments(); + expect([...args]).toEqual([1, 2, 3]); + } + }); + }); + + describe('allows access to `this` and `arguments` together', () => { + itSerializes('in sloppy mode function', { + in() { + function outer() { + with ({a: 5}) { + return () => [this, arguments]; // eslint-disable-line prefer-rest-params + } + } + return outer.call({x: 1}, 2, 3, 4); + }, + out: ` + ( + (b,c)=>function(){ + return a=>{ + with(a)return()=>[this,arguments] + } + }.apply(b,c) + )({x:1},function(){return arguments}(2,3,4))({a:5}) + `, + validate(fn) { + expect(fn).toBeFunction(); + const [that, args] = fn(); + expect(that).toEqual({x: 1}); + expect(args).toBeArguments(); + expect([...args]).toEqual([2, 3, 4]); + } + }); + + itSerializes('in strict mode function', { + in() { + function outer() { + with ({a: 5}) { + return () => { + 'use strict'; + + return [this, arguments]; // eslint-disable-line prefer-rest-params + }; + } + } + return outer.call({x: 1}, 2, 3, 4); + }, + out: ` + ( + (b,c)=>function(){ + return a=>{ + with(a)return()=>{ + "use strict"; + return[this,arguments] + } + } + }.apply(b,c) + )({x:1},function(){return arguments}(2,3,4))({a:5}) + `, + validate(fn) { + expect(fn).toBeFunction(); + const [that, args] = fn(); + expect(that).toEqual({x: 1}); + expect(args).toBeArguments(); + expect([...args]).toEqual([2, 3, 4]); + } + }); + }); + + itSerializes('allows access to `this` when `arguments` also in scope tree', { + in() { + function outer() { + let f; + with ({x: 1}) f = (0, () => [x, this]); // eslint-disable-line no-undef + return [f, () => arguments]; // eslint-disable-line prefer-rest-params + } + return outer.call({y: 2}, 3, 4, 5); + }, + out: `(()=>{ + const a=( + (b,c)=>function(){ + return[ + ()=>c, + a=>{with(a)return()=>[x,this]} + ] + }.call(b) + )({y:2},function(){return arguments}(3,4,5));return[a[1]({x:1}),a[0]] + })()`, + validate([fn1, fn2]) { + expect(fn1).toBeFunction(); + expect(fn1()).toEqual([1, {y: 2}]); + const args = fn2(); + expect(args).toBeArguments(); + expect([...args]).toEqual([3, 4, 5]); + } + }); + + itSerializes('alters scope when `with` object property changed', { + in() { + const obj = {x: 123}, + x = 456; + with (obj) { + return [() => x, obj]; + } + }, + out: `(()=>{ + const a={x:123}; + return[ + (x=>a=>{with(a)return()=>x})(456)(a), + a + ] + })()`, + validate([fn, obj]) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + obj.x = 789; + expect(fn()).toBe(789); + } + }); + + itSerializes('alters scope when `with` object property deleted', { + in() { + const obj = {x: 123}, + x = 456; + with (obj) { + return [() => x, obj]; + } + }, + out: `(()=>{ + const a={x:123}; + return[ + (x=>a=>{with(a)return()=>x})(456)(a), + a + ] + })()`, + validate([fn, obj]) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + delete obj.x; + expect(fn()).toBe(456); + } + }); + + describe('allows access to `eval`', () => { + /* eslint-disable no-eval */ + // This test fails because instrumentation replaces `eval` with `livepack_tracker.evalIndirect`, + // and therefore avoids getting `eval` from `obj.eval` in the `with ()`. + // TODO: Fix this + itSerializes.skip('global', { + in() { + const obj = {eval: 123}; + with (obj) { + return [() => eval, obj]; + } + }, + out: `(()=>{ + const a={eval:123}; + return[(a=>{with(a)return()=>eval})(a),a] + })()`, + validate([fn, obj]) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + delete obj.eval; + expect(fn()).toBe(eval); + } + }); + + itSerializes('local var', { + in() { + const eval = 123; // eslint-disable-line no-shadow-restricted-names + const obj = {eval: 456}; + with (obj) { + return [() => eval, obj]; + } + }, + out: `(()=>{ + const a={eval:456}; + return[(eval=>a=>{with(a)return()=>eval})(123)(a),a] + })()`, + validate([fn, obj]) { + expect(fn).toBeFunction(); + expect(fn()).toBe(456); + delete obj.eval; + expect(fn()).toBe(123); + } + }); + /* eslint-enable no-eval */ + }); + + describe('calls method with correct `this` value', () => { + itSerializes('executed before serialization', { + in() { + const obj = { + foo() { + return this === obj; + } + }; + + with (obj) { + return foo(); // eslint-disable-line no-undef + } + }, + out: 'true', + validate(bool) { + expect(bool).toBe(true); + } + }); + + itSerializes('executed before serialization when method is called `eval`', { + in() { + const obj = { + eval() { + return this === obj; + } + }; + + with (obj) { + return eval('1'); // eslint-disable-line no-eval + } + }, + out: 'true', + validate(bool) { + expect(bool).toBe(true); + } + }); + + itSerializes('in function which is serialized', { + in() { + const obj = { + foo() { + return this === obj; + } + }; + + with (obj) { + return () => foo(); // eslint-disable-line no-undef + } + }, + out: `(()=>{ + const a=(a=>[ + b=>a=b, + { + foo(){return this===a} + }.foo + ])(), + b={foo:a[1]}; + a[0](b); + return(a=>{with(a)return()=>foo()})(b) + })()`, + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(true); + } + }); + }); + + describe('invalid `with` values', () => { + // These tests ensure instrumentation does not alter normal behavior of `with ()` + it('`null`', () => { + const called = spy(); + expect(() => { + const val = null; + with (val) called(); + }).toThrowWithMessage(TypeError, 'Cannot convert undefined or null to object'); + expect(called).not.toHaveBeenCalled(); + }); + + it('`undefined`', () => { + const called = spy(); + expect(() => { + const val = undefined; + with (val) called(); + }).toThrowWithMessage(TypeError, 'Cannot convert undefined or null to object'); + expect(called).not.toHaveBeenCalled(); + }); + }); + + describe('non-object `with` values', () => { + itSerializes('function', { + in() { + with (function foo() {}) { + return () => name; // eslint-disable-line no-undef, no-restricted-globals + } + }, + out: '(a=>{with(a)return()=>name})(function foo(){})', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe('foo'); + } + }); + + itSerializes('string', { + in() { + with ('foo') { + return () => length; // eslint-disable-line no-undef, no-restricted-globals + } + }, + out: '(a=>{with(a)return()=>length})("foo")', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe(3); + } + }); + + itSerializes('number', { + in() { + with (123) { + return () => toString(); + } + }, + out: '(a=>{with(a)return()=>toString()})(123)', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe('123'); + } + }); + + itSerializes('bigint', { + in() { + with (12345678901234567890n) { + return () => toString(); + } + }, + out: '(a=>{with(a)return()=>toString()})(12345678901234567890n)', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe('12345678901234567890'); + } + }); + + itSerializes('symbol', { + in() { + with (Symbol('foo')) { + return () => description; // eslint-disable-line no-undef + } + }, + out: '(a=>{with(a)return()=>description})(Symbol("foo"))', + validate(fn) { + expect(fn).toBeFunction(); + expect(fn()).toBe('foo'); + } + }); + }); + + describe("does not disrupt instrumentation's internal vars", () => { + itSerializes('`livepack_tracker` + `livepack_setScopeId`', { + in: ` + const x = 123; + with ({livepack_tracker: 1, livepack_getScopeId: 2}) { + const y = 456; + module.exports = () => [x, y]; + } + `, + out: `(x=>b=>{ + with(b)return a=>()=>[x,a] + })(123)({livepack_tracker:1,livepack_getScopeId:2})(456)`, + validate(fn, {transpiled}) { + expect(fn).toBeFunction(); + expect(fn()).toEqual([123, 456]); + + // Check internal functions match the ones being tested for + expect(transpiled).toInclude('const [livepack_tracker, livepack_getScopeId] = require('); + } + }); + + itSerializes('`livepack_scopeId`', { + in: ` + const fns = []; + for (const x of [1, 2]) { + with ({livepack_scopeId_4: 3}) { + fns.push(() => x); + } + } + module.exports = fns; + `, + out: `(()=>{ + const a=x=>a=>{with(a)return()=>x}; + return[ + a(1)({livepack_scopeId_4:3}), + a(2)({livepack_scopeId_4:3}) + ] + })()`, + validate(fns, {transpiled}) { + expect(fns).toBeArrayOfSize(2); + const [fn1, fn2] = fns; + expect(fn1).toBeFunction(); + expect(fn1()).toBe(1); + expect(fn2()).toBe(2); + + // Check internal var matches the one being tested for + expect(transpiled) + .toInclude('for (const x of [1, 2]) {const livepack_scopeId_4 = livepack_getScopeId();'); + } + }); + + itSerializes('`livepack_temp` for `with` object', { + in: ` + with ({livepack_temp_6: 1, x: 2}) { + module.exports = () => x; + } + `, + out: '(a=>{with(a)return()=>x})({livepack_temp_6:1,x:2})', + validate(fn, {transpiled}) { + expect(fn).toBeFunction(); + expect(fn()).toBe(2); + + // Check internal var match the one being tested for + expect(transpiled.split('\n')[0]).toInclude(';let livepack_temp_6;'); + } + }); + + itSerializes('`livepack_temp` for class `super`', { + in: ` + class S { + foo() { + return 123; + } + } + with (Object.freeze({livepack_temp_20: 1})) module.exports = class C extends S { + foo() { + return super.foo(); + } + } + `, + out: `(()=>{ + "use strict"; + const a=Object, + b=a.setPrototypeOf, + c=class S{}, + d=c.prototype, + e=a.defineProperties, + f=b(class C extends null{},c), + g=(a=>[ + b=>a=b, + { + foo(){return Reflect.get(Object.getPrototypeOf(a.prototype),"foo",this).call(this)} + }.foo + ])(); + e(d,{foo:{value:{foo(){return 123}}.foo,writable:true,configurable:true}}); + g[0](f); + b(e(f.prototype,{foo:{value:g[1],writable:true,configurable:true}}),d); + return f + })()`, + validate(C, {transpiled}) { + expect(C).toBeFunction(); + const obj = new C(); + expect(obj.foo()).toBe(123); + + // Check temp var matches the one being tested for + expect(transpiled.split('\n')[0]).toInclude('let livepack_temp_20;'); + } + }); + + itSerializes('`livepack_getFnInfo`', { + in: ` + with ({livepack_getFnInfo_5: 1, livepack_getFnInfo_0: 2}) { + module.exports = () => 123; + } + `, + out: '()=>123', + validate(fn, {transpiled}) { + expect(fn).toBeFunction(); + expect(fn()).toBe(123); + + // Check internal functions match the ones being tested for + expect(transpiled).toInclude('function livepack_getFnInfo_5() {'); + expect(transpiled).toInclude('function livepack_getFnInfo_0() {'); + } + }); + }); + }); + + /* eslint-disable no-restricted-properties */ + describe('shimming `Object.prototype.__defineSetter__`', () => { + it('does not interfere with its normal functioning', () => { + const callee = spy(); + const obj = {}; + obj.__defineSetter__('foo', callee); + expect(callee).not.toHaveBeenCalled(); + obj.foo = 123; + expect(callee).toHaveBeenCalledOnce(); + expect(callee).toHaveBeenCalledWith(123); + }); + + itSerializesEqual('does not prevent serializing it', { + in: () => Object.prototype.__defineSetter__, + out: 'Object.prototype.__defineSetter__', + validate(defineSetter) { + expect(defineSetter).toBe(Object.prototype.__defineSetter__); + } + }); + }); + /* eslint-enable no-restricted-properties */ +});