diff --git a/lib/instrument/blocks.js b/lib/instrument/blocks.js index 1bb662a4..f398b507 100644 --- a/lib/instrument/blocks.js +++ b/lib/instrument/blocks.js @@ -199,6 +199,7 @@ function getOrCreateExternalVar(externalVars, block, varName, bindingOrExternalV varNode: bindingOrExternalVar.varNode, isReadFrom: false, isAssignedTo: false, + isNameLocked: false, argNames: bindingOrExternalVar.argNames, trails: [] }; diff --git a/lib/instrument/visitors/function.js b/lib/instrument/visitors/function.js index 67fcb644..315dd5cb 100644 --- a/lib/instrument/visitors/function.js +++ b/lib/instrument/visitors/function.js @@ -728,6 +728,7 @@ function createFunctionInfoFunction(fn, scopes, astJson, fnInfoVarNode, state) { return { isReadFrom: varProps.isReadFrom || undefined, isAssignedTo: varProps.isAssignedTo || undefined, + isNameLocked: varProps.isNameLocked || undefined, trails: varProps.trails }; }) diff --git a/lib/instrument/visitors/identifier.js b/lib/instrument/visitors/identifier.js index e9d4dba8..aa7aa91e 100644 --- a/lib/instrument/visitors/identifier.js +++ b/lib/instrument/visitors/identifier.js @@ -81,7 +81,7 @@ function ThisExpression(node, state) { // Ignore if internal to function, unless in class constructor or prototype class property // of class with super class. if (block.id < fn.id) { - recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, state); + recordExternalVar(binding, block, 'this', fn, [...state.trail], true, false, false, state); } else if (fn.hasSuperClass) { createArrayOrPush(fn.internalVars, 'this', [...state.trail]); } @@ -105,7 +105,7 @@ function NewTargetExpression(node, state) { const block = state.currentThisBlock; if (block.id < fn.id) { recordExternalVar( - block.bindings['new.target'], block, 'new.target', fn, [...state.trail], true, false, state + block.bindings['new.target'], block, 'new.target', fn, [...state.trail], true, false, false, state ); } } @@ -148,7 +148,7 @@ function visitIdentifier(node, varName, isReadFrom, isAssignedTo, state) { function resolveIdentifierInSecondPass(node, block, varName, fn, isReadFrom, isAssignedTo, state) { state.secondPass( resolveIdentifier, - node, block, varName, fn, [...state.trail], isReadFrom, isAssignedTo, state.isStrict, state + node, block, varName, fn, [...state.trail], isReadFrom, isAssignedTo, false, state.isStrict, state ); } @@ -161,14 +161,30 @@ function resolveIdentifierInSecondPass(node, block, varName, fn, isReadFrom, isA * @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 {boolean} isStrict - `true` if variable used in strict mode * @param {Object} state - State object * @returns {undefined} */ -function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssignedTo, isStrict, state) { +function resolveIdentifier( + node, block, varName, fn, trail, isReadFrom, isAssignedTo, isBehindWith, isStrict, state +) { // Find binding - let binding; + let binding, + bindingVarName = varName, + isWithBinding = false; do { + // Check for a `with () {}` block which can intercept any variable + if (block.bindings.with) { + // Ignore `with () {}` blocks internal to function + if (block.id >= fn.id) continue; + + binding = block.bindings.with; + bindingVarName = 'with'; + isWithBinding = true; + break; + } + binding = block.bindings[varName]; } while (!binding && (block = block.parent)); // eslint-disable-line no-cond-assign @@ -188,7 +204,8 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign } // Record external var - if (isAssignedTo && binding.isConst) { + const bindingIsAssignedTo = isAssignedTo && !isWithBinding; + if (bindingIsAssignedTo && binding.isConst) { // Record const violation fn.amendments.push({ type: binding.isSilentConst && !isStrict @@ -204,7 +221,16 @@ function resolveIdentifier(node, block, varName, fn, trail, isReadFrom, isAssign isAssignedTo = false; } - recordExternalVar(binding, block, varName, fn, trail, isReadFrom, isAssignedTo, state); + recordExternalVar( + binding, block, bindingVarName, fn, trail, isReadFrom, bindingIsAssignedTo, isBehindWith, state + ); + + // If is a `with () {}` block, continue to search for bindings further down the scope chain + if (isWithBinding) { + resolveIdentifier( + node, block.parent, varName, fn, trail, isReadFrom, isAssignedTo, true, isStrict, state + ); + } } /** @@ -216,14 +242,18 @@ 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, varName); const externalVar = getOrCreateExternalVar(fn.externalVars, block, varName, binding); if (isReadFrom) externalVar.isReadFrom = true; if (isAssignedTo) externalVar.isAssignedTo = true; - externalVar.trails.push(trail); + if (isBehindWith) externalVar.isNameLocked = true; + if (varName !== 'with') externalVar.trails.push(trail); } diff --git a/lib/serialize/parseFunction.js b/lib/serialize/parseFunction.js index 3615ae1e..77e2abfc 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -81,7 +81,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, + isNameLocked: !!varProps.isNameLocked + }; }) }); } @@ -865,11 +869,15 @@ 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, isNameLocked, 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 (isNameLocked) scopeDefVar.isNameLocked = true; } externalVars[varName].push(...trailsToNodes(fnNode, trails, varName));