Permalink
Browse files

Enforce TDZ within initializer of lexical declaration (refactored) (#…

…2733)

* [[FIX]] Enforce TDZ within initializer of lexical declaration

Fixes #2637

* [[FIX]] Enforce TDZ within class heritage definition

* [[FIX]] Enforce TDZ within for in/of head

Fixes gh-2693

* [[CHORE]] Refactor var initialization tracking

Conceptually, the initialization state of a given variable is a property
of that variable. Model that relationship in the code organization by
tracking variable initialization using a dedicated property on the
representation of the variable itself. Beyond improving code clarity
(the "label" objects remain the single source of truth for the state of
each variable), this also reduces memory allocation costs (no new
objects need to be created).
  • Loading branch information...
jugglinmike authored and rwaldron committed Oct 18, 2016
1 parent f7eb3d7 commit 8e9d40618a0a70c417778630c83d30dca68d189f
Showing with 154 additions and 18 deletions.
  1. +24 −4 src/jshint.js
  2. +18 −2 src/scope-manager.js
  3. +108 −7 tests/unit/core.js
  4. +4 −5 tests/unit/options.js
@@ -3568,10 +3568,6 @@ var JSHINT = (function() {
type: type,
token: t.token });
names.push(t.token);

if (lone && inexport) {
state.funct["(scope)"].setExported(t.token.value, t.token);
}
}
}
}
@@ -3594,6 +3590,19 @@ var JSHINT = (function() {
}
}

if (!prefix) {
for (t in tokens) {
if (tokens.hasOwnProperty(t)) {
t = tokens[t];
state.funct["(scope)"].initialize(t.id);

if (lone && inexport) {
state.funct["(scope)"].setExported(t.token.value, t.token);
}
}
}
}

statement.first = statement.first.concat(names);

if (state.tokens.next.id !== ",") {
@@ -3732,14 +3741,21 @@ var JSHINT = (function() {
state.funct["(scope)"].addlabel(this.name, {
type: "class",
token: state.tokens.curr });

} else if (state.tokens.next.identifier && state.tokens.next.value !== "extends") {
// BindingIdentifier(opt)
this.name = identifier();
this.namedExpr = true;
} else {
this.name = state.nameStack.infer();
}

classtail(this);

if (isStatement) {
state.funct["(scope)"].initialize(this.name);
}

return this;
}

@@ -4584,6 +4600,7 @@ var JSHINT = (function() {
// Import bindings are immutable (see ES6 8.1.1.5.5)
state.funct["(scope)"].addlabel(this.name, {
type: "const",
initialized: true,
token: state.tokens.curr });

if (state.tokens.next.value === ",") {
@@ -4610,6 +4627,7 @@ var JSHINT = (function() {
// Import bindings are immutable (see ES6 8.1.1.5.5)
state.funct["(scope)"].addlabel(this.name, {
type: "const",
initialized: true,
token: state.tokens.curr });
}
} else {
@@ -4635,6 +4653,7 @@ var JSHINT = (function() {
// Import bindings are immutable (see ES6 8.1.1.5.5)
state.funct["(scope)"].addlabel(importName, {
type: "const",
initialized: true,
token: state.tokens.curr });

if (state.tokens.next.value === ",") {
@@ -4699,6 +4718,7 @@ var JSHINT = (function() {
if (this.block) {
state.funct["(scope)"].addlabel(identifier, {
type: exportType,
initialized: true,
token: token });

state.funct["(scope)"].setExported(identifier, token);
@@ -611,12 +611,19 @@ var scopeManager = function(state, predefined, exported, declared) {
this.block.use(labelName, token);
},

initialize: function(labelName) {
if (_current["(labels)"][labelName]) {
_current["(labels)"][labelName]["(initialized)"] = true;
}
},

/**
* adds an indentifier to the relevant current scope and creates warnings/errors as necessary
* @param {string} labelName
* @param {Object} opts
* @param {String} opts.type - the type of the label e.g. "param", "var", "let, "const", "function"
* @param {Token} opts.token - the token pointing at the declaration
* @param {boolean} opts.initialized - whether the binding should be created in an "initialized" state.
*/
addlabel: function(labelName, opts) {

@@ -664,7 +671,9 @@ var scopeManager = function(state, predefined, exported, declared) {
}
}

scopeManagerInst.block.add(labelName, type, token, !isexported);
scopeManagerInst.block.add(
labelName, type, token, !isexported, opts.initialized
);

} else {

@@ -805,6 +814,12 @@ var scopeManager = function(state, predefined, exported, declared) {
token["(function)"] = _currentFunctBody;
_current["(usages)"][labelName]["(tokens)"].push(token);
}

// blockscoped vars can't be used within their initializer (TDZ)
var label = _current["(labels)"][labelName];
if (label && label["(blockscoped)"] && !label["(initialized)"]) {
error("E056", token, labelName, label["(type)"]);
}
},

reassign: function(labelName, token) {
@@ -827,10 +842,11 @@ var scopeManager = function(state, predefined, exported, declared) {
/**
* Adds a new variable
*/
add: function(labelName, type, tok, unused) {
add: function(labelName, type, tok, unused, initialized) {
_current["(labels)"][labelName] = {
"(type)" : type,
"(token)": tok,
"(initialized)": !!initialized,
"(blockscoped)": true,
"(unused)": unused };
},
@@ -925,12 +925,13 @@ exports.testConstRedeclaration = function (test) {
];

TestRun(test)
.addError(2, "'a' has already been declared.")
.addError(9, "'a' has already been declared.")
.addError(13, "'b' has already been declared.")
.test(src, {
esnext: true
});
.addError(2, "'a' has already been declared.")
.addError(6, "'a' was used before it was declared, which is illegal for 'const' variables.")
.addError(9, "'a' has already been declared.")
.addError(13, "'b' has already been declared.")
.test(src, {
esnext: true
});

test.done();
};
@@ -2088,4 +2089,104 @@ exports["destructuring in setter parameter"] = function (test) {
], { esversion: 6 });

test.done();
};
};

exports["TDZ within initializer of lexical declarations"] = function(test) {
var code = [
"let a = a;",
"const b = b;",
"let c = () => c;",
"const d = () => d;",
// line 5
"let e = {",
" x: e,",
" y: () => e",
"};",
"const f = {",
" x: f,",
" y: () => f",
"};",
// line 13
"let g, h = g;",
"const i = 0, j = i;",
"let [ k, l ] = l;",
"const [ m, n ] = n;",
// line 17
"let o = (() => o) + o;",
"const p = (() => p) + p;"
];

TestRun(test)
.addError(1, "'a' was used before it was declared, which is illegal for 'let' variables.")
.addError(2, "'b' was used before it was declared, which is illegal for 'const' variables.")
.addError(6, "'e' was used before it was declared, which is illegal for 'let' variables.")
.addError(10, "'f' was used before it was declared, which is illegal for 'const' variables.")
.addError(15, "'l' was used before it was declared, which is illegal for 'let' variables.")
.addError(16, "'n' was used before it was declared, which is illegal for 'const' variables.")
.addError(17, "'o' was used before it was declared, which is illegal for 'let' variables.")
.addError(18, "'p' was used before it was declared, which is illegal for 'const' variables.")
.test(code, { esversion: 6 });

test.done();
};

exports["TDZ within class heritage definition"] = function(test) {
var code = [
"let A = class extends A {};",
"let B = class extends { B } {};",
"let C = class extends { method() { return C; } } {};",
// line 4
"const D = class extends D {};",
"const E = class extends { E } {};",
"const F = class extends { method() { return F; } } {};",
// line 7
"class G extends G {}",
"class H extends { H } {}",
"class I extends { method() { return I; }} {}"
];

TestRun(test)
.addError(1, "'A' was used before it was declared, which is illegal for 'let' variables.")
.addError(2, "'B' was used before it was declared, which is illegal for 'let' variables.")
.addError(4, "'D' was used before it was declared, which is illegal for 'const' variables.")
.addError(5, "'E' was used before it was declared, which is illegal for 'const' variables.")
.addError(7, "'G' was used before it was declared, which is illegal for 'class' variables.")
.addError(8, "'H' was used before it was declared, which is illegal for 'class' variables.")
.test(code, { esversion: 6 });

test.done();
};

exports["TDZ within for in/of head"] = function(test) {
var code = [
"for (let a in a);",
"for (const b in b);",
"for (let c of c);",
"for (const d of d);",

// line 5
"for (let e in { e });",
"for (const f in { f });",
"for (let g of { g });",
"for (const h of { h });",

// line 9
"for (let i in { method() { return i; } });",
"for (const j in { method() { return j; } });",
"for (let k of { method() { return k; } });",
"for (const l of { method() { return l; } });"
];

TestRun(test)
.addError(1, "'a' was used before it was declared, which is illegal for 'let' variables.")
.addError(2, "'b' was used before it was declared, which is illegal for 'const' variables.")
.addError(3, "'c' was used before it was declared, which is illegal for 'let' variables.")
.addError(4, "'d' was used before it was declared, which is illegal for 'const' variables.")
.addError(5, "'e' was used before it was declared, which is illegal for 'let' variables.")
.addError(6, "'f' was used before it was declared, which is illegal for 'const' variables.")
.addError(7, "'g' was used before it was declared, which is illegal for 'let' variables.")
.addError(8, "'h' was used before it was declared, which is illegal for 'const' variables.")
.test(code, { esversion: 6 });

test.done();
};
@@ -116,7 +116,9 @@ exports.shadowEs6 = function (test) {
[344, "'zzi' has already been declared."],
[345, "'zzj' has already been declared."],
[349, "'zzl' has already been declared."],
[349, "'zzl' was used before it was declared, which is illegal for 'const' variables."],
[350, "'zzm' has already been declared."],
[350, "'zzm' was used before it was declared, which is illegal for 'let' variables."],
[364, "'zj' has already been declared."]
];

@@ -759,11 +761,8 @@ exports.undef = function (test) {

// block scope cannot use themselves in the declaration
TestRun(test)
// JSHint does not currently enforce the correct temporal dead zone
// semantics in this case. Once this is fixed, the following errors
// should be thrown:
//.addError(1, "'a' was used before it was declared, which is illegal for 'let' variables.")
//.addError(2, "'b' was used before it was declared, which is illegal for 'const' variables.")
.addError(1, "'a' was used before it was declared, which is illegal for 'let' variables.")
.addError(2, "'b' was used before it was declared, which is illegal for 'const' variables.")
.addError(5, "'e' is already defined.")
.test([
'let a = a;',

0 comments on commit 8e9d406

Please sign in to comment.