Fix tdz checks in transform-block-scoping plugin (#9498)

* Better tdz tests

- Use jest's expect.toThrow/expect.not.toThrow
- Add input/output tests

* Fix basic tdz (a = 2; let a)

Fixes #6848

* Make _guessExecutionStatusRelativeTo more robust

* Add tests

* Return less "unkown" execution status

* "function" execution status does not exist

* Fix recursive functions

* Update helper version

* "finally" blocks are always executed

* Typo
This commit is contained in:
Nicolò Ribaudo 2019-07-21 06:34:43 +02:00 committed by GitHub
parent 9bc9571381
commit fced5cea43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 507 additions and 122 deletions

View File

@ -817,18 +817,6 @@ helpers.taggedTemplateLiteralLoose = helper("7.0.0-beta.0")`
}
`;
helpers.temporalRef = helper("7.0.0-beta.0")`
import undef from "temporalUndefined";
export default function _temporalRef(val, name) {
if (val === undef) {
throw new ReferenceError(name + " is not defined - temporal dead zone");
} else {
return val;
}
}
`;
helpers.readOnlyError = helper("7.0.0-beta.0")`
export default function _readOnlyError(name) {
throw new Error("\\"" + name + "\\" is read-only");
@ -842,7 +830,24 @@ helpers.classNameTDZError = helper("7.0.0-beta.0")`
`;
helpers.temporalUndefined = helper("7.0.0-beta.0")`
export default {};
// This function isn't mean to be called, but to be used as a reference.
// We can't use a normal object because it isn't hoisted.
export default function _temporalUndefined() {}
`;
helpers.tdz = helper("7.5.5")`
export default function _tdzError(name) {
throw new ReferenceError(name + " is not defined - temporal dead zone");
}
`;
helpers.temporalRef = helper("7.0.0-beta.0")`
import undef from "temporalUndefined";
import err from "tdz";
export default function _temporalRef(val, name) {
return val === undef ? err(name) : val;
}
`;
helpers.slicedToArray = helper("7.0.0-beta.0")`

View File

@ -16,7 +16,7 @@ export default declare((api, opts) => {
throw new Error(`.throwIfClosureRequired must be a boolean, or undefined`);
}
if (typeof tdzEnabled !== "boolean") {
throw new Error(`.throwIfClosureRequired must be a boolean, or undefined`);
throw new Error(`.tdz must be a boolean, or undefined`);
}
return {
@ -33,11 +33,13 @@ export default declare((api, opts) => {
for (let i = 0; i < node.declarations.length; i++) {
const decl = node.declarations[i];
if (decl.init) {
const assign = t.assignmentExpression("=", decl.id, decl.init);
assign._ignoreBlockScopingTDZ = true;
nodes.push(t.expressionStatement(assign));
}
const assign = t.assignmentExpression(
"=",
decl.id,
decl.init || scope.buildUndefinedNode(),
);
assign._ignoreBlockScopingTDZ = true;
nodes.push(t.expressionStatement(assign));
decl.init = this.addHelper("temporalUndefined");
}
@ -181,6 +183,8 @@ const letReferenceBlockVisitor = traverse.visitors.merge([
// simply rename the variables.
if (state.loopDepth > 0) {
path.traverse(letReferenceFunctionVisitor, state);
} else {
path.traverse(tdzVisitor, state);
}
return path.skip();
},
@ -756,7 +760,7 @@ class BlockScoping {
closurify: false,
loopDepth: 0,
tdzEnabled: this.tdzEnabled,
addHelper: name => this.addHelper(name),
addHelper: name => this.state.addHelper(name),
};
if (isInLoop(this.blockPath)) {

View File

@ -1,12 +1,12 @@
import { types as t } from "@babel/core";
import { types as t, template } from "@babel/core";
function getTDZStatus(refPath, bindingPath) {
const executionStatus = bindingPath._guessExecutionStatusRelativeTo(refPath);
if (executionStatus === "before") {
return "inside";
} else if (executionStatus === "after") {
return "outside";
} else if (executionStatus === "after") {
return "inside";
} else {
return "maybe";
}
@ -41,7 +41,7 @@ export const visitor = {
if (bindingPath.isFunctionDeclaration()) return;
const status = getTDZStatus(path, bindingPath);
if (status === "inside") return;
if (status === "outside") return;
if (status === "maybe") {
const assert = buildTDZAssert(node, state);
@ -57,19 +57,8 @@ export const visitor = {
} else {
path.replaceWith(assert);
}
} else if (status === "outside") {
path.replaceWith(
t.throwStatement(
t.inherits(
t.newExpression(t.identifier("ReferenceError"), [
t.stringLiteral(
`${node.name} is not defined - temporal dead zone`,
),
]),
node,
),
),
);
} else if (status === "inside") {
path.replaceWith(template.ast`${state.addHelper("tdz")}("${node.name}")`);
}
},
@ -87,14 +76,14 @@ export const visitor = {
const id = ids[name];
if (isReference(id, path.scope, state)) {
nodes.push(buildTDZAssert(id, state));
nodes.push(id);
}
}
if (nodes.length) {
node._ignoreBlockScopingTDZ = true;
nodes.push(node);
path.replaceWithMultiple(nodes.map(t.expressionStatement));
path.replaceWithMultiple(nodes.map(n => t.expressionStatement(n)));
}
},
},

View File

@ -5,6 +5,7 @@ if (x) {
var innerScope = true;
var res = transform(code, {
configFile: false,
plugins: opts.plugins.concat([
function (b) {
var t = b.types;
@ -34,3 +35,4 @@ if (x) {
}`;
expect(res.code).toBe(expected);
expect(innerScope).toBe(false);

View File

@ -1,3 +1,5 @@
f();
expect(() => {
f();
const f = function f() {}
const f = function f() {}
}).toThrow(ReferenceError);

View File

@ -0,0 +1,3 @@
f();
const f = function f() {}

View File

@ -1,3 +0,0 @@
{
"throws": "f is not defined - temporal dead zone"
}

View File

@ -0,0 +1,3 @@
babelHelpers.tdz("f")();
var f = function f() {};

View File

@ -1 +1,3 @@
let { b: d } = { d }
expect(() => {
let { b: d } = { d }
}).toThrow(ReferenceError);

View File

@ -0,0 +1 @@
let { b: d } = { d }

View File

@ -1,3 +0,0 @@
{
"throws": "d is not defined - temporal dead zone"
}

View File

@ -0,0 +1,5 @@
var {
b: d
} = {
d: babelHelpers.tdz("d")
};

View File

@ -0,0 +1,7 @@
expect(() => {
function f() {
x;
}
let x;
f();
}).not.toThrow();

View File

@ -0,0 +1,6 @@
function f() {
x;
}
var x;
f();

View File

@ -0,0 +1,7 @@
expect(() => {
function f() {
x;
}
f();
let x;
}).toThrow(ReferenceError);

View File

@ -0,0 +1,5 @@
function f() {
x;
}
f();
let x;

View File

@ -0,0 +1,6 @@
function f() {
babelHelpers.tdz("x");
}
f();
var x;

View File

@ -0,0 +1,6 @@
expect(() => {
function f() { x }
Math.random() === 2 && f();
let x;
f();
}).not.toThrow();

View File

@ -0,0 +1,4 @@
function f() { x }
Math.random() === 2 && f();
let x;
f();

View File

@ -0,0 +1,9 @@
var x = babelHelpers.temporalUndefined;
function f() {
babelHelpers.temporalRef(x, "x");
}
Math.random() === 2 && f();
x = void 0;
f();

View File

@ -0,0 +1,5 @@
function f() { x }
Math.random() === 2 && f();
let x = 3;
expect(x).toBe(3);

View File

@ -0,0 +1,5 @@
function f() { x }
Math.random() === 2 && f();
let x = 3;
expect(x).toBe(3);

View File

@ -0,0 +1,9 @@
var x = babelHelpers.temporalUndefined;
function f() {
babelHelpers.temporalRef(x, "x");
}
Math.random() === 2 && f();
x = 3;
expect(x).toBe(3);

View File

@ -0,0 +1,29 @@
// "random" :)
let random = (i => {
const vals = [0, 0, 1, 1];
return () => vals[i++];
})(0);
expect(() => {
function f() { x }
random() && f();
let x;
}).not.toThrow();
expect(() => {
function f() { x }
random() || f();
let x;
}).toThrow(ReferenceError);
expect(() => {
function f() { x }
random() && f();
let x;
}).toThrow(ReferenceError);
expect(() => {
function f() { x }
random() || f();
let x;
}).not.toThrow();

View File

@ -0,0 +1,3 @@
function f() { x }
Math.random() && f();
let x;

View File

@ -0,0 +1,9 @@
var x = babelHelpers.temporalUndefined;
function f() {
babelHelpers.temporalRef(x, "x");
}
Math.random() && f();
x = void 0;
void 0;

View File

@ -0,0 +1,17 @@
expect(() => {
function f() {
return function() { x };
}
let g = f();
let x;
g();
}).not.toThrow();
expect(() => {
function f() {
return function() { x };
}
let g = f();
g();
let x;
}).toThrow(ReferenceError);

View File

@ -0,0 +1,5 @@
function f() {
return function() { x };
}
f();
let x;

View File

@ -0,0 +1,11 @@
var x = babelHelpers.temporalUndefined;
function f() {
return function () {
babelHelpers.temporalRef(x, "x");
};
}
f();
x = void 0;
void 0;

View File

@ -0,0 +1,9 @@
expect(() => {
function f(i) {
if (i) f(i - 1);
x;
}
let x;
f(3);
}).not.toThrow();

View File

@ -0,0 +1,7 @@
function f(i) {
if (i) f(i - 1);
x;
}
let x;
f(3);

View File

@ -0,0 +1,7 @@
function f(i) {
if (i) f(i - 1);
x;
}
var x;
f(3);

View File

@ -0,0 +1,9 @@
expect(() => {
function f(i) {
if (i) f(i - 1);
x;
}
f(3);
let x;
}).toThrow(ReferenceError);

View File

@ -0,0 +1,7 @@
function f(i) {
if (i) f(i - 1);
x;
}
f(3);
let x;

View File

@ -0,0 +1,7 @@
function f(i) {
if (i) f(i - 1);
babelHelpers.tdz("x");
}
f(3);
var x;

View File

@ -0,0 +1,12 @@
expect(() => {
function f(i) {
return () => {
x;
f(i - 1);
};
}
const g = f(1);
let x;
g();
}).not.toThrow();

View File

@ -0,0 +1,10 @@
function f(i) {
return () => {
x;
f(i - 1);
};
}
const g = f(1);
let x;
g();

View File

@ -0,0 +1,12 @@
var x = babelHelpers.temporalUndefined;
function f(i) {
return () => {
babelHelpers.temporalRef(x, "x");
f(i - 1);
};
}
var g = f(1);
x = void 0;
g();

View File

@ -0,0 +1,3 @@
function f() { x }
maybeCall(f);
let x;

View File

@ -0,0 +1,9 @@
var x = babelHelpers.temporalUndefined;
function f() {
babelHelpers.temporalRef(x, "x");
}
maybeCall(f);
x = void 0;
void 0;

View File

@ -1,3 +1,5 @@
f();
expect(() => {
f();
function f() {}
function f() {}
}).not.toThrow();

View File

@ -0,0 +1,3 @@
f();
function f() {}

View File

@ -0,0 +1,3 @@
f();
function f() {}

View File

@ -1,3 +1,5 @@
x = 3;
expect(() => {
x = 3;
var x;
var x;
}).not.toThrow();

View File

@ -0,0 +1,3 @@
x = 3;
var x;

View File

@ -0,0 +1,2 @@
x = 3;
var x;

View File

@ -1,3 +1,6 @@
{
"plugins": [["transform-block-scoping", { "tdz": true }]]
"plugins": [
["transform-block-scoping", { "tdz": true }],
["external-helpers", { "helperVersion": "7.1000.0" }]
]
}

View File

@ -1 +1,3 @@
let x = x;
expect(() => {
let x = x;
}).toThrow(ReferenceError);

View File

@ -0,0 +1 @@
let x = x;

View File

@ -1,3 +0,0 @@
{
"throws": "x is not defined - temporal dead zone"
}

View File

@ -0,0 +1 @@
var x = babelHelpers.tdz("x");

View File

@ -1,3 +1,8 @@
var a = 5;
if (a){ console.log(a); let a = 2; }
console.log(a);
expect(() => {
var a = 5;
if (a) {
a;
let a = 2;
}
a;
}).toThrow(ReferenceError);

View File

@ -0,0 +1,6 @@
var a = 5;
if (a) {
a;
let a = 2;
}
a;

View File

@ -1,3 +0,0 @@
{
"throws": "a is not defined - temporal dead zone"
}

View File

@ -0,0 +1,8 @@
var a = 5;
if (a) {
babelHelpers.tdz("a");
var _a = 2;
}
a;

View File

@ -0,0 +1,4 @@
expect(() => {
i = 2;
let i
}).toThrow(ReferenceError);

View File

@ -0,0 +1,2 @@
i = 2;
let i

View File

@ -0,0 +1,3 @@
babelHelpers.tdz("i");
i = 2;
var i;

View File

@ -1,2 +1,4 @@
i
let i
expect(() => {
i
let i
}).toThrow(ReferenceError);

View File

@ -1,3 +0,0 @@
{
"throws": "i is not defined - temporal dead zone"
}

View File

@ -0,0 +1,2 @@
babelHelpers.tdz("i");
var i;

View File

@ -102,7 +102,7 @@ function getConstantViolationsBefore(binding, path, functions) {
return violations.filter(violation => {
violation = violation.resolve();
const status = violation._guessExecutionStatusRelativeTo(path);
if (functions && status === "function") functions.push(violation);
if (functions && status === "unknown") functions.push(violation);
return status === "before";
});
}

View File

@ -206,6 +206,75 @@ export function willIMaybeExecuteBefore(target) {
return this._guessExecutionStatusRelativeTo(target) !== "after";
}
function getOuterFunction(path) {
return (path.scope.getFunctionParent() || path.scope.getProgramParent()).path;
}
function isExecutionUncertain(type, key) {
switch (type) {
// a && FOO
// a || FOO
case "LogicalExpression":
return key === "right";
// a ? FOO : FOO
// if (a) FOO; else FOO;
case "ConditionalExpression":
case "IfStatement":
return key === "consequent" || key === "alternate";
// while (a) FOO;
case "WhileStatement":
case "DoWhileStatement":
case "ForInStatement":
case "ForOfStatement":
return key === "body";
// for (a; b; FOO) FOO;
case "ForStatement":
return key === "body" || key === "update";
// switch (a) { FOO }
case "SwitchStatement":
return key === "cases";
// try { a } catch FOO finally { b }
case "TryStatement":
return key === "handler";
// var [ x = FOO ]
case "AssignmentPattern":
return key === "right";
// a?.[FOO]
case "OptionalMemberExpression":
return key === "property";
// a?.(FOO)
case "OptionalCallExpression":
return key === "arguments";
default:
return false;
}
}
function isExecutionUncertainInList(paths, maxIndex) {
for (let i = 0; i < maxIndex; i++) {
const path = paths[i];
if (isExecutionUncertain(path.parent.type, path.parentKey)) {
return true;
}
}
return false;
}
// TODO (Babel 8)
// This can be { before: boolean, after: boolean, unknown: boolean }.
// This allows transforms like the tdz one to treat cases when the status
// is both before and unknown/after like if it were before.
type RelativeExecutionStatus = "before" | "after" | "unknown";
/**
* Given a `target` check the execution status of it relative to the current path.
*
@ -213,108 +282,132 @@ export function willIMaybeExecuteBefore(target) {
* before or after the input `target` element.
*/
export function _guessExecutionStatusRelativeTo(target) {
export function _guessExecutionStatusRelativeTo(
target: NodePath,
): RelativeExecutionStatus {
// check if the two paths are in different functions, we can't track execution of these
const targetFuncParent =
target.scope.getFunctionParent() || target.scope.getProgramParent();
const selfFuncParent =
this.scope.getFunctionParent() || target.scope.getProgramParent();
const funcParent = {
this: getOuterFunction(this),
target: getOuterFunction(target),
};
// here we check the `node` equality as sometimes we may have different paths for the
// same node due to path thrashing
if (targetFuncParent.node !== selfFuncParent.node) {
const status = this._guessExecutionStatusRelativeToDifferentFunctions(
targetFuncParent,
if (funcParent.target.node !== funcParent.this.node) {
return this._guessExecutionStatusRelativeToDifferentFunctions(
funcParent.target,
);
if (status) {
return status;
} else {
target = targetFuncParent.path;
}
}
const targetPaths = target.getAncestry();
if (targetPaths.indexOf(this) >= 0) return "after";
const paths = {
target: target.getAncestry(),
this: this.getAncestry(),
};
const selfPaths = this.getAncestry();
// If this is an ancestor of the target path,
// e.g. f(g); where this is f and target is g.
if (paths.target.indexOf(this) >= 0) return "after";
if (paths.this.indexOf(target) >= 0) return "before";
// get ancestor where the branches intersect
let commonPath;
let targetIndex;
let selfIndex;
for (selfIndex = 0; selfIndex < selfPaths.length; selfIndex++) {
const selfPath = selfPaths[selfIndex];
targetIndex = targetPaths.indexOf(selfPath);
if (targetIndex >= 0) {
commonPath = selfPath;
break;
const commonIndex = { target: 0, this: 0 };
while (!commonPath && commonIndex.this < paths.this.length) {
const path = paths.this[commonIndex.this];
commonIndex.target = paths.target.indexOf(path);
if (commonIndex.target >= 0) {
commonPath = path;
} else {
commonIndex.this++;
}
}
if (!commonPath) {
return "before";
throw new Error(
"Internal Babel error - The two compared nodes" +
" don't appear to belong to the same program.",
);
}
// get the relationship paths that associate these nodes to their common ancestor
const targetRelationship = targetPaths[targetIndex - 1];
const selfRelationship = selfPaths[selfIndex - 1];
if (!targetRelationship || !selfRelationship) {
return "before";
if (
isExecutionUncertainInList(paths.this, commonIndex.this - 1) ||
isExecutionUncertainInList(paths.target, commonIndex.target - 1)
) {
return "unknown";
}
const divergence = {
this: paths.this[commonIndex.this - 1],
target: paths.target[commonIndex.target - 1],
};
// container list so let's see which one is after the other
// e.g. [ THIS, TARGET ]
if (
targetRelationship.listKey &&
targetRelationship.container === selfRelationship.container
divergence.target.listKey &&
divergence.this.listKey &&
divergence.target.container === divergence.this.container
) {
return targetRelationship.key > selfRelationship.key ? "before" : "after";
return divergence.target.key > divergence.this.key ? "before" : "after";
}
// otherwise we're associated by a parent node, check which key comes before the other
const keys = t.VISITOR_KEYS[commonPath.type];
const targetKeyPosition = keys.indexOf(targetRelationship.key);
const selfKeyPosition = keys.indexOf(selfRelationship.key);
return targetKeyPosition > selfKeyPosition ? "before" : "after";
const keyPosition = {
this: keys.indexOf(divergence.this.parentKey),
target: keys.indexOf(divergence.target.parentKey),
};
return keyPosition.target > keyPosition.this ? "before" : "after";
}
// Used to avoid infinite recursion in cases like
// function f() { if (false) f(); }
// f();
// It also works with indirect recursion.
const executionOrderCheckedNodes = new WeakSet();
export function _guessExecutionStatusRelativeToDifferentFunctions(
targetFuncParent,
) {
const targetFuncPath = targetFuncParent.path;
if (!targetFuncPath.isFunctionDeclaration()) return;
target: NodePath,
): RelativeExecutionStatus {
if (!target.isFunctionDeclaration()) return "unknown";
// so we're in a completely different function, if this is a function declaration
// then we can be a bit smarter and handle cases where the function is either
// a. not called at all (part of an export)
// b. called directly
const binding = targetFuncPath.scope.getBinding(targetFuncPath.node.id.name);
const binding = target.scope.getBinding(target.node.id.name);
// no references!
if (!binding.references) return "before";
const referencePaths: Array<NodePath> = binding.referencePaths;
// verify that all of the references are calls
for (const path of referencePaths) {
if (path.key !== "callee" || !path.parentPath.isCallExpression()) {
return;
}
}
let allStatus;
// verify that all the calls have the same execution status
for (const path of referencePaths) {
// if a reference is a child of the function we're checking against then we can
// safely ignore it
const childOfFunction = !!path.find(
path => path.node === targetFuncPath.node,
);
const childOfFunction = !!path.find(path => path.node === target.node);
if (childOfFunction) continue;
if (path.key !== "callee" || !path.parentPath.isCallExpression()) {
// This function is passed as a reference, so we don't
// know when it will be called.
return "unknown";
}
// Prevent infinte loops in recursive functions
if (executionOrderCheckedNodes.has(path.node)) continue;
executionOrderCheckedNodes.add(path.node);
const status = this._guessExecutionStatusRelativeTo(path);
if (allStatus) {
if (allStatus !== status) return;
executionOrderCheckedNodes.delete(path.node);
if (allStatus && allStatus !== status) {
return "unknown";
} else {
allStatus = status;
}