Skip to content

Commit

Permalink
[[FIX]] Parse for-in/of head LHS as asnmt target
Browse files Browse the repository at this point in the history
Previously, the left-hand side of the `for-in/of` head re-used the code
path for `var` declaration lists. This was undesirable for a number of
reasons:

- it made the `var` statement's parsing logic more complex
- it tended to produce inaccurate error messages, e.g. "destructuring
  binding is available in ES6"
- it disallowed syntactically valid code, e.g. `for (a.b in {}) {}` or
  `for ([x[y]] of []) {}`

Refactor the parsing logic of `for-in/of` heads to more faithfully match
the actual language grammar. Update the expected errors in the project's
unit test suite according to the more accurate messages that this change
produces.  Simplify `var` statement parsing logic by removing the code
intended to support this condition. Maintain support for initializers
and the comma operator in order to improve error messages in the case of
common programming mistakes.
  • Loading branch information
jugglinmike committed Apr 30, 2017
1 parent b075919 commit da52ad9
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 63 deletions.
77 changes: 52 additions & 25 deletions src/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -3649,10 +3649,6 @@ var JSHINT = (function() {
var inexport = context && context.inexport;
var tokens, lone, value;

// If the `implied` option is set, bindings are set differently.
var implied = context && context.implied;
var report = !(context && context.ignore);

this.first = [];
for (;;) {
var names = [];
Expand All @@ -3664,7 +3660,7 @@ var JSHINT = (function() {
lone = true;
}

if (!(prefix && implied) && report && state.option.varstmt) {
if (state.option.varstmt) {
warning("W132", this);
}

Expand All @@ -3673,7 +3669,7 @@ var JSHINT = (function() {
for (var t in tokens) {
if (tokens.hasOwnProperty(t)) {
t = tokens[t];
if (!implied && state.funct["(global)"] && !state.impliedClosure()) {
if (state.funct["(global)"] && !state.impliedClosure()) {
if (predefined[t.id] === false) {
warning("W079", t.token, t.id);
} else if (state.option.futurehostile === false) {
Expand All @@ -3684,20 +3680,12 @@ var JSHINT = (function() {
}
}
if (t.id) {
if (implied === "for") {
state.funct["(scope)"].addlabel(t.id, {
type: "var",
token: t.token });

if (!state.funct["(scope)"].has(t.id)) {
if (report) warning("W088", t.token, t.id);
}
state.funct["(scope)"].block.use(t.id, t.token);
} else {
state.funct["(scope)"].addlabel(t.id, {
type: "var",
token: t.token });

if (lone && inexport) {
state.funct["(scope)"].setExported(t.id, t.token);
}
if (lone && inexport) {
state.funct["(scope)"].setExported(t.id, t.token);
}
names.push(t.token);
}
Expand All @@ -3709,7 +3697,7 @@ var JSHINT = (function() {

advance("=");
if (peek(0).id === "=" && state.tokens.next.identifier) {
if (!prefix && report &&
if (!prefix &&
!state.funct["(params)"] ||
state.funct["(params)"].indexOf(state.tokens.next.value) === -1) {
warning("W120", state.tokens.next, state.tokens.next.value);
Expand All @@ -3718,7 +3706,7 @@ var JSHINT = (function() {
var id = state.tokens.prev;
// don't accept `in` in expression if prefix is used for ForIn/Of loop.
value = expression(prefix ? 120 : 10);
if (value && !prefix && report && !state.funct["(loopage)"] && value.type === "undefined") {
if (value && !prefix && !state.funct["(loopage)"] && value.type === "undefined") {
warning("W080", id, id.value);
}
if (lone) {
Expand Down Expand Up @@ -4259,6 +4247,8 @@ var JSHINT = (function() {
var comma; // First comma punctuator at level 0
var initializer; // First initializer at level 0
var bindingPower;
var targets;
var target;

// If initial token is a BindingPattern, count it as such.
if (checkPunctuators(state.tokens.next, ["{", "["])) ++level;
Expand Down Expand Up @@ -4286,7 +4276,6 @@ var JSHINT = (function() {
bindingPower = 0;
}

var ok = !(initializer || comma);
if (initializer) {
error("W133", comma, nextop.value, "initializer is forbidden");
}
Expand All @@ -4305,10 +4294,48 @@ var JSHINT = (function() {
state.funct["(scope)"].stack();
state.tokens.curr.fud({ prefix: true });
} else {
// Parse as a var statement, with implied bindings. Ignore errors if an error
// was already reported
Object.create(varstatement).fud({ prefix: true, implied: "for", ignore: !ok });
targets = [];

// The following parsing logic recognizes initializers and the comma
// operator despite the fact that they are not supported by the
// grammar. Doing so allows JSHint to emit more a meaningful error
// message (i.e. W133) in response to a common programming mistake.
do {
if (checkPunctuators(state.tokens.next, ["{", "["])) {
destructuringPattern({ assignment: true }).forEach(function(elem) {
this.push(elem.token);
}, targets);
} else {
target = expression(120);

if (target.type === "(identifier)") {
targets.push(target);
}

checkLeftSideAssign(target, nextop);
}

if (checkPunctuator(state.tokens.next, "=")) {
advance("=");
expression(120);
}

if (checkPunctuator(state.tokens.next, ",")) {
advance(",");
}
} while (state.tokens.next !== nextop);

// In the event of a syntax error, do no issue warnings regarding the
// implicit creation of bindings.
if (!initializer && !comma) {
targets.forEach(function(token) {
if (!state.funct["(scope)"].has(token.value)) {
warning("W088", token, token.value);
}
});
}
}

advance(nextop.value);
// The binding power is variable because for-in statements accept any
// Expression in this position, while for-of statements are limited to
Expand Down
5 changes: 0 additions & 5 deletions tests/test262/expectations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,6 @@ language/statements/for-in/decl-gen.js
language/statements/for-in/dstr-array-rest-init.js
language/statements/for-in/head-const-bound-names-fordecl-tdz.js
language/statements/for-in/head-let-bound-names-fordecl-tdz.js
language/statements/for-in/head-lhs-cover.js
language/statements/for-in/head-lhs-let.js
language/statements/for-in/head-var-bound-names-let.js
language/statements/for/decl-cls.js
Expand Down Expand Up @@ -1143,7 +1142,6 @@ language/statements/return/S12.9_A1_T7.js
language/statements/return/S12.9_A1_T8.js
language/statements/return/S12.9_A1_T9.js
language/statements/for-of/arguments-mapped-mutation.js
language/statements/for-of/body-put-error.js
language/statements/for-of/decl-cls.js
language/statements/for-of/decl-fun.js
language/statements/for-of/decl-gen.js
Expand Down Expand Up @@ -1177,7 +1175,6 @@ language/statements/for-of/dstr-array-rest-iter-rtrn-close.js
language/statements/for-of/dstr-array-rest-iter-thrw-close-err.js
language/statements/for-of/dstr-array-rest-iter-thrw-close.js
language/statements/for-of/dstr-array-rest-lref-err.js
language/statements/for-of/dstr-array-rest-lref.js
language/statements/for-of/dstr-array-rest-nested-array-iter-thrw-close-skip.js
language/statements/for-of/dstr-array-rest-nested-array-null.js
language/statements/for-of/dstr-array-rest-nested-array-undefined-hole.js
Expand Down Expand Up @@ -1227,8 +1224,6 @@ language/statements/for-of/dstr-var-ary-ptrn-rest-obj-id.js
language/statements/for-of/dstr-var-ary-ptrn-rest-obj-prop-id.js
language/statements/for-of/head-const-bound-names-fordecl-tdz.js
language/statements/for-of/head-let-bound-names-fordecl-tdz.js
language/statements/for-of/head-lhs-cover.js
language/statements/for-of/head-lhs-member.js
language/statements/for-of/head-var-bound-names-let.js
language/statements/switch/S12.11_A2_T1.js
language/statements/switch/scope-lex-close-case.js
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ exports.testForIn = function (test) {
];

TestRun(test)
.addError(2, "Expected an identifier and instead saw 'i'.")
.addError(2, "Bad assignment.")
.test(src);

src = [
Expand Down Expand Up @@ -744,6 +744,20 @@ exports.testForIn = function (test) {
.addError(5, "Invalid for-in loop left-hand-side: initializer is forbidden.")
.test(src, { esnext: true });

TestRun(test, "Left-hand side as MemberExpression")
.test([
"for (x.y in {}) {}",
"for (x[z] in {}) {}",
]);

TestRun(test, "Left-hand side as MemberExpression (invalid)")
.addError(1, "Bad assignment.", {character: 10})
.addError(2, "Bad assignment.", {character: 13})
.test([
"for (x+y in {}) {}",
"for ((this) in {}) {}"
]);

test.done();
};

Expand Down

0 comments on commit da52ad9

Please sign in to comment.