From eb14f1da008cacfec5b8c2a98ca6ed3e4ba0d138 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 8 Feb 2015 01:27:22 +1100 Subject: [PATCH] implement optional TDZ - fixes #563 --- lib/6to5/transformation/file.js | 4 ++- .../templates/default-parameter.js | 2 +- .../templates/temporal-assert-defined.js | 6 ++++ .../templates/temporal-undefined.js | 1 + .../transformers/es6/block-scoping-tdz.js | 24 +++++++--------- .../transformers/es6/block-scoping.js | 28 +++++++++++++++++-- .../transformers/es6/parameters.default.js | 28 ++++++++++++------- lib/6to5/transformation/transformers/index.js | 4 ++- .../es6-block-scoping-tdz-fail/assignment.js | 3 ++ .../es6-block-scoping-tdz-fail/call-2.js | 7 +++++ .../es6-block-scoping-tdz-fail/call-3.js | 7 +++++ .../es6-block-scoping-tdz-fail/call.js | 3 ++ .../es6-block-scoping-tdz-fail/defaults.js | 3 ++ .../es6-block-scoping-tdz-fail/options.json | 4 +++ .../es6-block-scoping-tdz-fail/update.js | 3 ++ .../es6-block-scoping-tdz-pass/assignment.js | 3 ++ .../es6-block-scoping-tdz-pass/call.js | 7 +++++ .../es6-block-scoping-tdz-pass/defaults.js | 3 ++ .../es6-block-scoping-tdz-pass/options.json | 3 ++ .../es6-block-scoping-tdz-pass/update.js | 3 ++ .../temporal-dead-zone/actual.js | 2 -- .../temporal-dead-zone/options.json | 4 --- 22 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 lib/6to5/transformation/templates/temporal-assert-defined.js create mode 100644 lib/6to5/transformation/templates/temporal-undefined.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/assignment.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/call-2.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/call-3.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/call.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/defaults.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/options.json create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-fail/update.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-pass/assignment.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-pass/call.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-pass/defaults.js create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-pass/options.json create mode 100644 test/fixtures/transformation/es6-block-scoping-tdz-pass/update.js delete mode 100644 test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/actual.js delete mode 100644 test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/options.json diff --git a/lib/6to5/transformation/file.js b/lib/6to5/transformation/file.js index 17fb4dfe87..6c3a6c62a3 100644 --- a/lib/6to5/transformation/file.js +++ b/lib/6to5/transformation/file.js @@ -54,7 +54,9 @@ File.helpers = [ "get", "set", "class-call-check", - "object-destructuring-empty" + "object-destructuring-empty", + "temporal-undefined", + "temporal-assert-defined" ]; File.validOptions = [ diff --git a/lib/6to5/transformation/templates/default-parameter.js b/lib/6to5/transformation/templates/default-parameter.js index 74fecfd0f2..4e77b53153 100644 --- a/lib/6to5/transformation/templates/default-parameter.js +++ b/lib/6to5/transformation/templates/default-parameter.js @@ -1 +1 @@ -var VARIABLE_NAME = ARGUMENTS[ARGUMENT_KEY] === undefined ? DEFAULT_VALUE : ARGUMENTS[ARGUMENT_KEY]; +let VARIABLE_NAME = ARGUMENTS[ARGUMENT_KEY] === undefined ? DEFAULT_VALUE : ARGUMENTS[ARGUMENT_KEY]; diff --git a/lib/6to5/transformation/templates/temporal-assert-defined.js b/lib/6to5/transformation/templates/temporal-assert-defined.js new file mode 100644 index 0000000000..97ec85d0bd --- /dev/null +++ b/lib/6to5/transformation/templates/temporal-assert-defined.js @@ -0,0 +1,6 @@ +(function (val, name, undef) { + if (val === undef) { + throw new ReferenceError(name + " is not defined - temporal dead zone"); + } + return true; +}) diff --git a/lib/6to5/transformation/templates/temporal-undefined.js b/lib/6to5/transformation/templates/temporal-undefined.js new file mode 100644 index 0000000000..b4100a5972 --- /dev/null +++ b/lib/6to5/transformation/templates/temporal-undefined.js @@ -0,0 +1 @@ +({}) diff --git a/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js b/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js index df09f17e01..df8002b671 100644 --- a/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js +++ b/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js @@ -12,22 +12,18 @@ var visitor = { // declared node is different in this scope if (scope.getBinding(node.name) !== declared) return; - var declaredLoc = declared.loc; - var referenceLoc = node.loc; + var assert = t.callExpression( + state.file.addHelper("temporal-assert-defined"), + [node, t.literal(node.name), state.file.addHelper("temporal-undefined")] + ); - if (!declaredLoc || !referenceLoc) return; + this.skip(); - // does this reference appear on a line before the declaration? - var before = referenceLoc.start.line < declaredLoc.start.line; - - if (referenceLoc.start.line === declaredLoc.start.line) { - // this reference appears on the same line - // check it appears before the declaration - before = referenceLoc.start.col < declaredLoc.start.col; - } - - if (before) { - throw state.file.errorWithNode(node, "Temporal dead zone - accessing a variable before it's initialized"); + if (t.isAssignmentExpression(parent) || t.isUpdateExpression(parent)) { + if (parent._ignoreBlockScopingTDZ) return; + this.parentPath.replaceNode(t.sequenceExpression([assert, parent])); + } else { + return t.logicalExpression("&&", assert, node); } } }; diff --git a/lib/6to5/transformation/transformers/es6/block-scoping.js b/lib/6to5/transformation/transformers/es6/block-scoping.js index b52a39f8bc..5acadcc184 100644 --- a/lib/6to5/transformation/transformers/es6/block-scoping.js +++ b/lib/6to5/transformation/transformers/es6/block-scoping.js @@ -17,7 +17,7 @@ var isLet = function (node, parent) { if (node.kind !== "let") return false; // https://github.com/6to5/6to5/issues/255 - if (!t.isFor(parent) || t.isFor(parent) && parent.left !== node) { + if (isLetInitable(node, parent)) { for (var i = 0; i < node.declarations.length; i++) { var declar = node.declarations[i]; declar.init = declar.init || t.identifier("undefined"); @@ -29,6 +29,10 @@ var isLet = function (node, parent) { return true; }; +var isLetInitable = function (node, parent) { + return !t.isFor(parent) || t.isFor(parent) && parent.left !== node; +}; + var isVar = function (node, parent) { return t.isVariableDeclaration(node, { kind: "var" }) && !isLet(node, parent); }; @@ -39,8 +43,26 @@ var standardizeLets = function (declars) { } }; -exports.VariableDeclaration = function (node, parent) { - isLet(node, parent); +exports.VariableDeclaration = function (node, parent, scope, file) { + if (!isLet(node, parent)) return; + + if (isLetInitable(node) && file.transformers["es6.blockScopingTDZ"].canRun()) { + var nodes = [node]; + + for (var i = 0; i < node.declarations.length; i++) { + var decl = node.declarations[i]; + if (decl.init) { + var assign = t.assignmentExpression("=", decl.id, decl.init); + assign._ignoreBlockScopingTDZ = true; + nodes.push(t.expressionStatement(assign)); + } + decl.init = file.addHelper("temporal-undefined"); + } + + node._blockHoist = 2; + + return nodes; + } }; exports.Loop = function (node, parent, scope, file) { diff --git a/lib/6to5/transformation/transformers/es6/parameters.default.js b/lib/6to5/transformation/transformers/es6/parameters.default.js index b56b6ee34f..9c3d53239d 100644 --- a/lib/6to5/transformation/transformers/es6/parameters.default.js +++ b/lib/6to5/transformation/transformers/es6/parameters.default.js @@ -23,7 +23,7 @@ var iifeVisitor = { } }; -exports.Function = function (node, parent, scope) { +exports.Function = function (node, parent, scope, file) { if (!hasDefaults(node)) return; t.ensureBlock(node); @@ -37,11 +37,26 @@ exports.Function = function (node, parent, scope) { var state = { iife: false, scope: scope }; + var pushDefNode = function (left, right, i) { + var defNode = util.template("default-parameter", { + VARIABLE_NAME: left, + DEFAULT_VALUE: right, + ARGUMENT_KEY: t.literal(i), + ARGUMENTS: argsIdentifier + }, true); + file.checkNode(defNode); + defNode._blockHoist = node.params.length - i; + body.push(defNode); + }; + for (var i = 0; i < node.params.length; i++) { var param = node.params[i]; if (!t.isAssignmentPattern(param)) { - lastNonDefaultParam = +i + 1; + lastNonDefaultParam = i + 1; + if (file.transformers["es6.blockScopingTDZ"].canRun()) { + pushDefNode(param, t.identifier("undefined"), i); + } continue; } @@ -58,14 +73,7 @@ exports.Function = function (node, parent, scope) { } } - var defNode = util.template("default-parameter", { - VARIABLE_NAME: left, - DEFAULT_VALUE: right, - ARGUMENT_KEY: t.literal(+i), - ARGUMENTS: argsIdentifier - }, true); - defNode._blockHoist = node.params.length - i; - body.push(defNode); + pushDefNode(left, right, i); } // we need to cut off all trailing default parameters diff --git a/lib/6to5/transformation/transformers/index.js b/lib/6to5/transformation/transformers/index.js index d63ba08c61..56d7c84772 100644 --- a/lib/6to5/transformation/transformers/index.js +++ b/lib/6to5/transformation/transformers/index.js @@ -46,6 +46,9 @@ module.exports = { "es6.constants": require("./es6/constants"), + // needs to be before `es6.blockScoping` as default parameters have a TDZ + "es6.parameters.default": require("./es6/parameters.default"), + // needs to be before `_aliasFunction` due to block scopes sometimes being wrapped in a // closure "es6.blockScoping": require("./es6/block-scoping"), @@ -53,7 +56,6 @@ module.exports = { // needs to be after `es6.blockScoping` due to needing `letReferences` set on blocks "es6.blockScopingTDZ": require("./es6/block-scoping-tdz"), - "es6.parameters.default": require("./es6/parameters.default"), "es6.parameters.rest": require("./es6/parameters.rest"), "es6.destructuring": require("./es6/destructuring"), diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/assignment.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/assignment.js new file mode 100644 index 0000000000..1cbd4a9d77 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/assignment.js @@ -0,0 +1,3 @@ +a = 1; + +let a = 2; diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-2.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-2.js new file mode 100644 index 0000000000..5ca4d5a118 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-2.js @@ -0,0 +1,7 @@ +function b() { + assert.equals(a, 1); +} + +let a = 1; + +b(); diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-3.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-3.js new file mode 100644 index 0000000000..0cf8af3c51 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call-3.js @@ -0,0 +1,7 @@ +function b() { + assert.equals(a, 1); +} + +b(); + +let a = 1; diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/call.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call.js new file mode 100644 index 0000000000..9649d59c5d --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/call.js @@ -0,0 +1,3 @@ +a; + +let a = 1; diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/defaults.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/defaults.js new file mode 100644 index 0000000000..3c4ca4143a --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/defaults.js @@ -0,0 +1,3 @@ +function foo(bar = bar2, bar2) {} + +foo(); diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/options.json b/test/fixtures/transformation/es6-block-scoping-tdz-fail/options.json new file mode 100644 index 0000000000..835e6ee178 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/options.json @@ -0,0 +1,4 @@ +{ + "optional": "es6.blockScopingTDZ", + "throws": "is not defined - temporal dead zone" +} diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-fail/update.js b/test/fixtures/transformation/es6-block-scoping-tdz-fail/update.js new file mode 100644 index 0000000000..14b66520e2 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-fail/update.js @@ -0,0 +1,3 @@ +a++; + +let a = 1; diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-pass/assignment.js b/test/fixtures/transformation/es6-block-scoping-tdz-pass/assignment.js new file mode 100644 index 0000000000..8df3c85142 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-pass/assignment.js @@ -0,0 +1,3 @@ +let a = 1; +a = 2; +assert.equal(a, 2); diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-pass/call.js b/test/fixtures/transformation/es6-block-scoping-tdz-pass/call.js new file mode 100644 index 0000000000..a6b9c61003 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-pass/call.js @@ -0,0 +1,7 @@ +let a = 1; + +function b() { + return a + 1; +} + +assert.equal(b(), 2); diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-pass/defaults.js b/test/fixtures/transformation/es6-block-scoping-tdz-pass/defaults.js new file mode 100644 index 0000000000..ad3928a373 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-pass/defaults.js @@ -0,0 +1,3 @@ +function foo(bar, bar2 = bar) {} + +foo(); diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-pass/options.json b/test/fixtures/transformation/es6-block-scoping-tdz-pass/options.json new file mode 100644 index 0000000000..725a1cc809 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-pass/options.json @@ -0,0 +1,3 @@ +{ + "optional": "es6.blockScopingTDZ" +} diff --git a/test/fixtures/transformation/es6-block-scoping-tdz-pass/update.js b/test/fixtures/transformation/es6-block-scoping-tdz-pass/update.js new file mode 100644 index 0000000000..b38b49bbd1 --- /dev/null +++ b/test/fixtures/transformation/es6-block-scoping-tdz-pass/update.js @@ -0,0 +1,3 @@ +let a = 1; +a++; +assert.equal(a, 2); diff --git a/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/actual.js b/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/actual.js deleted file mode 100644 index 34a3ccd844..0000000000 --- a/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/actual.js +++ /dev/null @@ -1,2 +0,0 @@ -qux; -let qux = 456; diff --git a/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/options.json b/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/options.json deleted file mode 100644 index 47e8262e80..0000000000 --- a/test/fixtures/transformation/es6-block-scoping/temporal-dead-zone/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "throws": "Temporal dead zone - accessing a variable before it's initialized", - "optional": ["es6.blockScopingTDZ"] -}