Repository: jshint/jshint
Pull request #2468 (2015-06-16 07:29:27)

Scope manager - extract to external file
@jugglinmike 

This PR is based on #2435 and pulls scope manager into a seperate file. Since #2435 is rebased to a single commit, this could be pulled as-is instead of that PR if you wish.

jugglinmike: Awesome, @lukeapage! I'm really looking forward to reviewing this version. Forgive me, though: it may be a few days before I have the opportunity.
jugglinmike: Hey @lukeapage, this time I focused on the tests. I have two pieces of feedback
based on what I learned there.

First, treating unreferenced `globals` as unused is a breaking change. While
this could be a great way for users to avoid accumulating unnecessary
configuration, I'm not so sure about including outside of a major release.

The second is a lot trickier. JSHint's default behavior is to treat `var`
statements as block scoped... kind of. Based on modifications to existing tests
(the so-called "regression" tests in particular), it looks like your patch
makes this behavior more consistent. Whenever I see new errors added to
existing tests, I get a little nervous because it means the change is
backwards-incompatable. In most cases, this is fine because your patch is
correctly identifying invalid JavaScript--I'm prepared to defend the decision
to issue new warnings for code that was never valid to begin with. But this is
a little trickier for two reasons: it's an opinionated rule, and it is enabled
by default. This means that many users may not even be aware that it is being
enforced, so I have more sympathy if their code starts to fail lint.

To me, the only way to reconcile that second point is to defer landing this
until we're ready for 3.0 . Do you agree? If so, I'd be happy to create a
`v3.0.0` feature branch (I think it's high time we got started on that anyway)
and make this the first patch to land there. But maybe I'm being too
conservative (or even misinterpreting the changes I'm seeing), so don't
hesitate to tell me that, either!
lukeapage: > First, treating unreferenced globals as unused is a breaking change. While
> this could be a great way for users to avoid accumulating unnecessary
> configuration, I'm not so sure about including outside of a major release.

I've been looking through my test changes and I don't see any change to unreferenced globals being unused.
At the moment this causes 2 warnings
```
/* jshint unused: true */
/* globals a */
var b;
```
and it still produces 2 warnings after my change.
lukeapage: > The second is a lot trickier. JSHint's default behavior is to treat var
> statements as block scoped... kind of. Based on modifications to existing tests
> (the so-called "regression" tests in particular), it looks like your patch
> makes this behavior more consistent.

I think its this bugfix in particular: https://github.com/jshint/jshint/issues/2409

There were lots of
```
try {
   var foo = window.foo.bar;
}
catch(e) {
   foo = polyfill;
}
function() {
  foo();
}
```

> Whenever I see new errors added to
> existing tests, I get a little nervous because it means the change is
> backwards-incompatable. In most cases, this is fine because your patch is
> correctly identifying invalid JavaScript--I'm prepared to defend the decision
> to issue new warnings for code that was never valid to begin with. But this is
> a little trickier for two reasons: it's an opinionated rule, and it is enabled
> by default. This means that many users may not even be aware that it is being
> enforced, so I have more sympathy if their code starts to fail lint.

I can undo this particular bug fix.. I'll look at how hard that would be, I'm hoping it's quite easy. then I could label the bugfix as for jshint 3?

There are alot of changes here and given the current pace of development, I can't see jshint 3 coming particularly soon..
lukeapage: > I can undo this particular bug fix..

I've added code so that I don't add warnings. I recommend we keep this in a separate commit, branch a jshint 3 branch and then revert that commit.

If you are happy, I'd like to see this land asap so it can be worked on top of
lukeapage: Hold on, I made a mistake, will comment again once I've corrected the commit
lukeapage: Okay corrected problem with 8cf4234cec743e261015b59e041449e98f880424 - thats the one that needs undoing on a 3 branch.

I also found a bug in my work which is fixed in the next commit  - 53b24706644092305a51e2e7166c8fadbdea3496

Patch:
diff --git a/src/jshint.js b/src/jshint.js
index 89ed886f..b8a58d1d 100644
--- a/src/jshint.js
+++ b/src/jshint.js
@@ -32,15 +32,16 @@
 /*global console:true */
 /*exported console */
 
-var _        = require("lodash");
-var events   = require("events");
-var vars     = require("./vars.js");
-var messages = require("./messages.js");
-var Lexer    = require("./lex.js").Lexer;
-var reg      = require("./reg.js");
-var state    = require("./state.js").state;
-var style    = require("./style.js");
-var options  = require("./options.js");
+var _            = require("lodash");
+var events       = require("events");
+var vars         = require("./vars.js");
+var messages     = require("./messages.js");
+var Lexer        = require("./lex.js").Lexer;
+var reg          = require("./reg.js");
+var state        = require("./state.js").state;
+var style        = require("./style.js");
+var options      = require("./options.js");
+var scopeManager = require("./scope-manager.js");
 
 // We need this module here because environments such as IE and Rhino
 // don't necessarilly expose the 'console' API and browserify uses
@@ -74,7 +75,6 @@ var JSHINT = (function() {
     },
 
     declared, // Globals that were declared using /*global ... */ syntax.
-    exported, // Variables that are used outside of the current file.
 
     functionicity = [
       "closure", "exception", "global", "label",
@@ -83,8 +83,6 @@ var JSHINT = (function() {
 
     functions, // All of the functions
 
-    global, // The global scope
-    implied, // Implied globals
     inblock,
     indent,
     lookahead,
@@ -93,9 +91,7 @@ var JSHINT = (function() {
     membersOnly,
     predefined,    // Global variables defined by option
 
-    scope,  // The current scope
     stack,
-    unuseds,
     urls,
 
     extraModules = [],
@@ -321,12 +317,6 @@ var JSHINT = (function() {
     };
   }
 
-  function isundef(scope, code, token, a) {
-    if (!state.ignored[code] && state.option.undef !== false) {
-      JSHINT.undefs.push([scope, code, token, a]);
-    }
-  }
-
   function removeIgnoredMessages() {
     var ignored = state.ignoredLines;
 
@@ -411,82 +401,10 @@ var JSHINT = (function() {
     return i;
   }
 
-  // adds an indentifier to the relevant current scope and creates warnings/errors as necessary
-  // name: string
-  // opts: { type: string, token: token, isblockscoped: bool }
-  function addlabel(name, opts) {
-
-    var type  = opts.type;
-    var token = opts.token;
-    var isblockscoped = opts.isblockscoped;
-
-    // Define label in the current function in the current scope.
-    if (type === "exception") {
-      if (_.has(state.funct["(context)"], name)) {
-        if (state.funct[name] !== true && !state.option.node) {
-          warning("W002", state.tokens.next, name);
-        }
-      }
-    }
-
-    if (_.has(state.funct, name) && !state.funct["(global)"]) {
-      if (state.funct[name] === true) {
-        if (state.option.latedef) {
-          if ((state.option.latedef === true && _.contains([state.funct[name], type], "unction")) ||
-              !_.contains([state.funct[name], type], "unction")) {
-            warning("W003", state.tokens.next, name);
-          }
-        }
-      } else {
-        if ((!state.option.shadow || _.contains([ "inner", "outer" ], state.option.shadow)) &&
-            type !== "exception" || state.funct["(blockscope)"].getlabel(name)) {
-          warning("W004", state.tokens.next, name);
-        }
-      }
-    }
-
-    if (state.funct["(context)"] && _.has(state.funct["(context)"], name) && type !== "function") {
-      if (state.option.shadow === "outer") {
-        warning("W123", state.tokens.next, name);
-      }
-    }
-
-    // if the identifier is blockscoped (a let or a const), add it only to the current blockscope
-    if (isblockscoped) {
-      state.funct["(blockscope)"].current.add(name, type, state.tokens.curr);
-      if (state.funct["(blockscope)"].atTop() && exported[name]) {
-        state.tokens.curr.exported = true;
-      }
-    } else {
-      state.funct["(blockscope)"].shadow(name);
-      state.funct[name] = type;
-
-      if (token) {
-        state.funct["(tokens)"][name] = token;
-      }
-
-      if (state.funct["(global)"]) {
-        global[name] = state.funct;
-        if (_.has(implied, name)) {
-          if (state.option.latedef) {
-            if ((state.option.latedef === true &&
-                _.contains([state.funct[name], type], "unction")) ||
-                !_.contains([state.funct[name], type], "unction")) {
-              warning("W003", state.tokens.next, name);
-            }
-          }
-
-          delete implied[name];
-        }
-      } else {
-        scope[name] = state.funct;
-      }
-    }
-  }
-
   function doOption() {
     var nt = state.tokens.next;
     var body = nt.body.split(",").map(function(s) { return s.trim(); });
+
     var predef = {};
     if (nt.type === "globals") {
       body.forEach(function(g, idx) {
@@ -534,7 +452,7 @@ var JSHINT = (function() {
           return;
         }
 
-        exported[e] = true;
+        state.funct["(scope)"].addExported(e);
       });
     }
 
@@ -873,7 +791,7 @@ var JSHINT = (function() {
       }
       isLetExpr = true;
       // create a new block scope we use only for the current expression
-      state.funct["(blockscope)"].stack();
+      state.funct["(scope)"].stack();
       advance("let");
       advance("(");
       state.tokens.prev.fud();
@@ -950,7 +868,7 @@ var JSHINT = (function() {
       }
     }
     if (isLetExpr) {
-      state.funct["(blockscope)"].unstack();
+      state.funct["(scope)"].unstack();
     }
 
     state.nameStack.pop();
@@ -1109,9 +1027,7 @@ var JSHINT = (function() {
         // detect increment/decrement of a const
         // in the case of a.b, right will be the "." punctuator
         if (this.right && this.right.identifier) {
-          if (state.funct["(blockscope)"].labeltype(this.right.value) === "const") {
-            error("E013", this, this.right.value);
-          }
+          state.funct["(scope)"].block.modify(this.right.value, this);
         }
       }
 
@@ -1336,15 +1252,11 @@ var JSHINT = (function() {
             warning("W121", left, nativeObject);
         }
 
-        if (predefined[left.value] === false &&
-            scope[left.value]["(global)"] === true) {
-          warning("W020", left);
-        } else if (left["function"]) {
-          warning("W021", left, left.value);
-        }
-
-        if (state.funct["(blockscope)"].labeltype(left.value) === "const") {
-          error("E013", left, left.value);
+        if (left.identifier) {
+          // reassign also calls modify
+          // but we are specific in order to catch function re-assignment
+          // and globals re-assignment
+          state.funct["(scope)"].block.reassign(left.value, left);
         }
 
         if (left.id === ".") {
@@ -1360,8 +1272,8 @@ var JSHINT = (function() {
         } else if (left.id === "[") {
           if (state.tokens.curr.left.first) {
             state.tokens.curr.left.first.forEach(function(t) {
-              if (t && state.funct[t.value] === "const") {
-                error("E013", t, t.value);
+              if (t && t.identifier) {
+                state.funct["(scope)"].block.modify(t.value, t);
               }
             });
           } else if (!left.left) {
@@ -1375,7 +1287,7 @@ var JSHINT = (function() {
           that.right = expression(10);
           return that;
         } else if (left.identifier && !isReserved(left)) {
-          if (state.funct[left.value] === "exception") {
+          if (state.funct["(scope)"].labeltype(left.value) === "exception") {
             warning("W022", left);
           }
           state.nameStack.set(left);
@@ -1448,9 +1360,7 @@ var JSHINT = (function() {
       // detect increment/decrement of a const
       // in the case of a.b, left will be the "." punctuator
       if (left && left.identifier) {
-        if (state.funct["(blockscope)"].labeltype(left.value) === "const") {
-          error("E013", this, left.value);
-        }
+        state.funct["(scope)"].block.modify(left.value, left);
       }
 
       this.left = left;
@@ -1583,7 +1493,7 @@ var JSHINT = (function() {
   }
 
   function statement() {
-    var i = indent, r, s = scope, t = state.tokens.next;
+    var i = indent, r, t = state.tokens.next, hasOwnScope = false;
 
     if (t.id === ";") {
       advance(";");
@@ -1605,8 +1515,10 @@ var JSHINT = (function() {
     if (t.identifier && !res && peek().id === ":") {
       advance();
       advance(":");
-      scope = Object.create(s);
-      addlabel(t.value, { type: "label" });
+
+      hasOwnScope = true;
+      state.funct["(scope)"].stack();
+      state.funct["(scope)"].block.addBreakLabel(t.value, { token: state.tokens.curr });
 
       if (!state.tokens.next.labelled && state.tokens.next.value !== "{") {
         warning("W028", state.tokens.next, t.value, state.tokens.next.value);
@@ -1658,7 +1570,9 @@ var JSHINT = (function() {
     // Restore the indentation.
 
     indent = i;
-    scope = s;
+    if (hasOwnScope) {
+      state.funct["(scope)"].unstack();
+    }
     return r;
   }
 
@@ -1750,16 +1664,12 @@ var JSHINT = (function() {
       b = inblock,
       old_indent = indent,
       m,
-      s = scope,
       t,
       line,
       d;
 
     inblock = ordinary;
 
-    if (!ordinary || !state.option.funcscope)
-      scope = Object.create(scope);
-
     t = state.tokens.next;
 
     var metrics = state.funct["(metrics)"];
@@ -1770,7 +1680,7 @@ var JSHINT = (function() {
       advance("{");
 
       // create a new block scope
-      state.funct["(blockscope)"].stack();
+      state.funct["(scope)"].stack();
 
       line = state.tokens.curr.line;
       if (state.tokens.next.id !== "}") {
@@ -1808,11 +1718,13 @@ var JSHINT = (function() {
 
       advance("}", t);
 
-      state.funct["(blockscope)"].unstack();
+      state.funct["(scope)"].unstack();
 
       indent = old_indent;
     } else if (!ordinary) {
       if (isfunc) {
+        state.funct["(scope)"].stack();
+
         m = {};
         if (stmt && !isfatarrow && !state.inMoz()) {
           error("W118", state.tokens.curr, "function closure expressions");
@@ -1832,6 +1744,8 @@ var JSHINT = (function() {
             warning("E007");
           }
         }
+
+        state.funct["(scope)"].unstack();
       } else {
         error("E021", state.tokens.next, "{", state.tokens.next.value);
       }
@@ -1840,6 +1754,7 @@ var JSHINT = (function() {
       // check to avoid let declaration not within a block
       // though is fine inside for loop initializer section
       state.funct["(noblockscopedvar)"] = state.tokens.next.id !== "for";
+      state.funct["(scope)"].stack();
 
       if (!stmt || state.option.curly) {
         warning("W116", state.tokens.next, "{", state.tokens.next.value);
@@ -1851,6 +1766,7 @@ var JSHINT = (function() {
       a = [statement()];
       indent -= state.option.indent;
 
+      state.funct["(scope)"].unstack();
       delete state.funct["(noblockscopedvar)"];
     }
 
@@ -1869,7 +1785,6 @@ var JSHINT = (function() {
       state.funct["(verb)"] = null;
     }
 
-    if (!ordinary || !state.option.funcscope) scope = s;
     inblock = b;
     if (ordinary && state.option.noempty && (!a || a.length === 0)) {
       warning("W035", state.tokens.prev);
@@ -1890,18 +1805,6 @@ var JSHINT = (function() {
     }
   }
 
-
-  function note_implied(tkn) {
-    var name = tkn.value;
-    var desc = Object.getOwnPropertyDescriptor(implied, name);
-
-    if (!desc)
-      implied[name] = [tkn.line];
-    else
-      desc.value.push(tkn.line);
-  }
-
-
   // Build the syntax table by declaring the syntactic elements of the language.
 
   type("(number)", function() {
@@ -1919,11 +1822,6 @@ var JSHINT = (function() {
 
     nud: function() {
       var v = this.value;
-      // s will be either the function object 'state.funct' that the identifier points at
-      //   or it will be a boolean if it is a predefined variable
-      var s = scope[v];
-      var f;
-      var block;
 
       // If this identifier is the lone parameter to a shorthand "fat arrow"
       // function definition, i.e.
@@ -1937,107 +1835,8 @@ var JSHINT = (function() {
         return this;
       }
 
-      block = state.funct["(blockscope)"].getlabel(v);
-
-      if (typeof s === "function") {
-        // Protection against accidental inheritance.
-        s = undefined;
-      } else if (!block && typeof s === "boolean") {
-        f = state.funct;
-        state.funct = functions[0];
-        addlabel(v, { type: "var" });
-        s = state.funct;
-        state.funct = f;
-      }
-
-      // The name is in scope and defined in the current function or it exists in the blockscope.
-      if (state.funct === s || block) {
-        // Change 'unused' to 'var', and reject labels.
-        // the name is in a block scope.
-        switch (block ? block[v]["(type)"] : state.funct[v]) {
-        case "unused":
-          if (block) block[v]["(type)"] = "var";
-          else state.funct[v] = "var";
-          break;
-        case "unction":
-          if (block) block[v]["(type)"] = "function";
-          else state.funct[v] = "function";
-          this["function"] = true;
-          break;
-        case "const":
-          // consts can only exist inside block
-          // because they are never added to the scope, s
-          block[v]["(unused)"] = false;
-          break;
-        case "function":
-          this["function"] = true;
-          break;
-        case "label":
-          warning("W037", state.tokens.curr, v);
-          break;
-        }
-      } else {
-        // If the name is already defined in the current
-        // function, but not as outer, then there is a scope error.
-
-        switch (state.funct[v]) {
-        case "closure":
-        case "function":
-        case "var":
-        case "unused":
-          warning("W038", state.tokens.curr, v);
-          break;
-        case "label":
-          warning("W037", state.tokens.curr, v);
-          break;
-        case "outer":
-        case "global":
-          break;
-        default:
-          // If the name is defined in an outer function, make an outer entry,
-          // and if it was unused, make it var.
-          if (s === true) {
-            state.funct[v] = true;
-          } else if (s === null) {
-            warning("W039", state.tokens.curr, v);
-            note_implied(state.tokens.curr);
-          } else if (typeof s !== "object") {
-            // if we're in a list comprehension, variables are declared
-            // locally and used before being defined. So we check
-            // the presence of the given variable in the comp array
-            // before declaring it undefined.
-
-            if (!state.funct["(comparray)"].check(v)) {
-              isundef(state.funct, "W117", state.tokens.curr, v);
-            }
-
-            // Explicitly mark the variable as used within function scopes
-            if (!state.funct["(global)"]) {
-              state.funct[v] = true;
-            }
-
-            note_implied(state.tokens.curr);
-          } else {
-            switch (s[v]) {
-            case "function":
-            case "unction":
-              this["function"] = true;
-              s[v] = "closure";
-              state.funct[v] = s["(global)"] ? "global" : "outer";
-              break;
-            case "var":
-            case "unused":
-              s[v] = "closure";
-              state.funct[v] = s["(global)"] ? "global" : "outer";
-              break;
-            case "closure":
-              state.funct[v] = s["(global)"] ? "global" : "outer";
-              break;
-            case "label":
-              warning("W037", state.tokens.curr, v);
-            }
-          }
-        }
+      if (!state.funct["(comparray)"].check(v)) {
+        state.funct["(scope)"].block.use(v, state.tokens.curr);
       }
       return this;
     },
@@ -2430,7 +2229,8 @@ var JSHINT = (function() {
         default:
           if (c.id !== "function") {
             i = c.value.substr(0, 1);
-            if (state.option.newcap && (i < "A" || i > "Z") && !_.has(global, c.value)) {
+            if (state.option.newcap && (i < "A" || i > "Z") &&
+              !state.funct["(scope)"].isPredefined(c.value)) {
               warning("W055", state.tokens.curr);
             }
           }
@@ -2869,6 +2669,7 @@ var JSHINT = (function() {
    *                                  single-argument shorthand.
    * @param {bool} [options.parsedOpening] Whether the opening parenthesis has
    *                                       already been parsed.
+   * @returns {Array.<{ id: string, token: Token}>}
    */
   function functionparams(options) {
     var next;
@@ -2881,8 +2682,7 @@ var JSHINT = (function() {
     var loneArg = options && options.loneArg;
 
     if (loneArg && loneArg.identifier === true) {
-      addlabel(loneArg.value, { type: "unused", token: loneArg });
-      return [loneArg.value];
+      return [ { id: loneArg.value, token: loneArg }];
     }
 
     next = state.tokens.next;
@@ -2902,16 +2702,14 @@ var JSHINT = (function() {
         for (t in tokens) {
           t = tokens[t];
           if (t.id) {
-            params.push(t.id);
-            addlabel(t.id, { type: "unused", token: t.token });
+            params.push({ id: t.id, token: t });
           }
         }
       } else {
         if (checkPunctuators(state.tokens.next, ["..."])) pastRest = true;
         ident = identifier(true);
         if (ident) {
-          params.push(ident);
-          addlabel(ident, { type: "unused", token: state.tokens.curr });
+          params.push({ id: ident, token: state.tokens.curr });
         } else {
           // Skip invalid parameter.
           while (!checkPunctuators(state.tokens.next, [",", ")"])) advance();
@@ -2944,12 +2742,11 @@ var JSHINT = (function() {
     }
   }
 
-  function functor(name, token, scope, overwrites) {
+  function functor(name, token, overwrites) {
     var funct = {
       "(name)"      : name,
       "(breakage)"  : 0,
       "(loopage)"   : 0,
-      "(scope)"     : scope,
       "(tokens)"    : {},
       "(properties)": {},
 
@@ -2961,7 +2758,7 @@ var JSHINT = (function() {
       "(metrics)"   : null,
       "(statement)" : null,
       "(context)"   : null,
-      "(blockscope)": null,
+      "(scope)"     : null,
       "(comparray)" : null,
       "(generator)" : null,
       "(params)"    : null
@@ -2978,7 +2775,7 @@ var JSHINT = (function() {
     _.extend(funct, overwrites);
 
     if (funct["(context)"]) {
-      funct["(blockscope)"] = funct["(context)"]["(blockscope)"];
+      funct["(scope)"] = funct["(context)"]["(scope)"];
       funct["(comparray)"]  = funct["(context)"]["(comparray)"];
     }
 
@@ -3056,7 +2853,6 @@ var JSHINT = (function() {
     var f, name, statement, classExprBinding, isGenerator, isArrow;
     var oldOption = state.option;
     var oldIgnored = state.ignored;
-    var oldScope  = scope;
 
     if (options) {
       name = options.name;
@@ -3068,9 +2864,8 @@ var JSHINT = (function() {
 
     state.option = Object.create(state.option);
     state.ignored = Object.create(state.ignored);
-    scope = Object.create(scope);
 
-    state.funct = functor(name || state.nameStack.infer(), state.tokens.next, scope, {
+    state.funct = functor(name || state.nameStack.infer(), state.tokens.next, {
       "(statement)": statement,
       "(context)":   state.funct,
       "(generator)": isGenerator
@@ -3081,17 +2876,31 @@ var JSHINT = (function() {
 
     functions.push(state.funct);
 
+    var params = functionparams(options);
+    var paramsIds;
+    if (params) {
+      paramsIds = _.map(params, function(token) {
+        return token.id;
+      });
+    }
+
+    // create the param scope and adds the labels to it
+    state.funct["(scope)"].stackParams(params, true);
+    state.funct["(params)"] = paramsIds;
+    state.funct["(metrics)"].verifyMaxParametersPerFunction(paramsIds);
+
     if (name) {
-      addlabel(name, { type: "function" });
+      // So that the function is available to itself and referencing itself is not
+      // seen as a closure, add the function name to the current scope, but do not
+      // test for unused (unused: false)
+      state.funct["(scope)"].funct.add(name, "function", state.tokens.curr, false);
     }
 
     if (classExprBinding) {
-      addlabel(classExprBinding, { type: "function" });
+      // if we are inside a class expression, make the class name available to sub functions
+      state.funct["(scope)"].funct.add(classExprBinding, "function", state.tokens.curr, false);
     }
 
-    state.funct["(params)"] = functionparams(options);
-    state.funct["(metrics)"].verifyMaxParametersPerFunction(state.funct["(params)"]);
-
     if (isArrow) {
       if (!state.option.esnext) {
         warning("W119", state.tokens.curr, "arrow function syntax (=>)");
@@ -3112,17 +2921,12 @@ var JSHINT = (function() {
     state.funct["(metrics)"].verifyMaxStatementsPerFunction();
     state.funct["(metrics)"].verifyMaxComplexityPerFunction();
     state.funct["(unusedOption)"] = state.option.unused;
-
-    scope = oldScope;
     state.option = oldOption;
     state.ignored = oldIgnored;
     state.funct["(last)"] = state.tokens.curr.line;
     state.funct["(lastcharacter)"] = state.tokens.curr.character;
 
-    _.map(Object.keys(state.funct), function(key) {
-      if (key[0] === "(") return;
-      state.funct["(blockscope)"].unshadow(key);
-    });
+    state.funct["(scope)"].unstack(); // also does usage and label checks
 
     state.funct = state.funct["(context)"];
 
@@ -3453,7 +3257,7 @@ var JSHINT = (function() {
         warning("W118", state.tokens.next, "let block");
       }
       advance("(");
-      state.funct["(blockscope)"].stack();
+      state.funct["(scope)"].stack();
       letblock = true;
     } else if (state.funct["(noblockscopedvar)"]) {
       error("E048", state.tokens.curr, isConst ? "Const" : "Let");
@@ -3468,41 +3272,14 @@ var JSHINT = (function() {
       } else {
         tokens = [ { id: identifier(), token: state.tokens.curr } ];
         lone = true;
-        if (inexport) {
-          exported[state.tokens.curr.value] = true;
-          state.tokens.curr.exported = true;
-        }
-      }
-      for (var t in tokens) {
-        if (tokens.hasOwnProperty(t)) {
-          t = tokens[t];
-          if (state.inESNext()) {
-            // only look in the latest scope because we can shadow
-            if (state.funct["(blockscope)"].current.labeltype(t.id) === "const") {
-              warning("E011", null, t.id);
-            }
-          }
-          if (state.funct["(global)"]) {
-            if (predefined[t.id] === false) {
-              warning("W079", t.token, t.id);
-            }
-          }
-          if (t.id && !state.funct["(noblockscopedvar)"]) {
-            addlabel(t.id, {
-              type: isConst ? "const" : "unused",
-              token: t.token,
-              isblockscoped: true });
-            names.push(t.token);
-          }
-        }
       }
 
-      statement.first = statement.first.concat(names);
-
       if (!prefix && isConst && state.tokens.next.id !== "=") {
         warning("E012", state.tokens.curr, state.tokens.curr.value);
       }
 
+      // absorb the assignment before processing the label so that use of the label
+      // can be detected as before declaration
       if (state.tokens.next.id === "=") {
         advance("=");
         if (!prefix && state.tokens.next.id === "undefined") {
@@ -3520,6 +3297,29 @@ var JSHINT = (function() {
         }
       }
 
+      for (var t in tokens) {
+        if (tokens.hasOwnProperty(t)) {
+          t = tokens[t];
+          if (state.funct["(global)"]) {
+            if (predefined[t.id] === false) {
+              warning("W079", t.token, t.id);
+            }
+          }
+          if (t.id && !state.funct["(noblockscopedvar)"]) {
+            state.funct["(scope)"].addlabel(t.id, {
+              type: type,
+              token: t.token });
+            names.push(t.token);
+
+            if (lone && inexport) {
+              state.funct["(scope)"].setExported(t.token.value, t.token);
+            }
+          }
+        }
+      }
+
+      statement.first = statement.first.concat(names);
+
       if (state.tokens.next.id !== ",") {
         break;
       }
@@ -3529,7 +3329,7 @@ var JSHINT = (function() {
       advance(")");
       block(true, true);
       statement.block = true;
-      state.funct["(blockscope)"].unstack();
+      state.funct["(scope)"].unstack();
     }
 
     return statement;
@@ -3563,51 +3363,6 @@ var JSHINT = (function() {
       } else {
         tokens = [ { id: identifier(), token: state.tokens.curr } ];
         lone = true;
-        if (inexport) {
-          exported[state.tokens.curr.value] = true;
-          state.tokens.curr.exported = true;
-        }
-      }
-      for (var t in tokens) {
-        if (tokens.hasOwnProperty(t)) {
-          t = tokens[t];
-          if (state.inESNext()) {
-            // because var is function scoped, look in the whole function
-            if (state.funct["(blockscope)"].labeltype(t.id) === "const") {
-              warning("E011", null, t.id);
-            }
-          }
-          if (!implied && state.funct["(global)"]) {
-            if (predefined[t.id] === false) {
-              warning("W079", t.token, t.id);
-            } else if (state.option.futurehostile === false) {
-              if ((!state.inES5() && vars.ecmaIdentifiers[5][t.id] === false) ||
-                  (!state.inESNext() && vars.ecmaIdentifiers[6][t.id] === false)) {
-                warning("W129", t.token, t.id);
-              }
-            }
-          }
-          if (t.id) {
-            if (implied === "for") {
-              var ident = t.token.value;
-              switch (state.funct[ident]) {
-              case "unused":
-                state.funct[ident] = "var";
-                break;
-              case "var":
-                break;
-              default:
-                if (!state.funct["(blockscope)"].getlabel(ident) &&
-                    !(scope[ident] || {})[ident]) {
-                  if (report) warning("W088", t.token, ident);
-                }
-              }
-            } else {
-              addlabel(t.id, { type: "unused", token: t.token });
-            }
-            names.push(t.token);
-          }
-        }
       }
 
       if (!prefix && report && state.option.varstmt) {
@@ -3616,6 +3371,8 @@ var JSHINT = (function() {
 
       this.first = this.first.concat(names);
 
+      // absorb the assignment before processing the label so that use of the label
+      // can be detected as before declaration
       if (state.tokens.next.id === "=") {
         state.nameStack.set(state.tokens.curr);
 
@@ -3639,6 +3396,40 @@ var JSHINT = (function() {
         }
       }
 
+      for (var t in tokens) {
+        if (tokens.hasOwnProperty(t)) {
+          t = tokens[t];
+          if (!implied && state.funct["(global)"]) {
+            if (predefined[t.id] === false) {
+              warning("W079", t.token, t.id);
+            } else if (state.option.futurehostile === false) {
+              if ((!state.inES5() && vars.ecmaIdentifiers[5][t.id] === false) ||
+                (!state.inESNext() && vars.ecmaIdentifiers[6][t.id] === false)) {
+                warning("W129", t.token, t.id);
+              }
+            }
+          }
+          if (t.id) {
+            if (implied === "for") {
+
+              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);
+              }
+            }
+            names.push(t.token);
+          }
+        }
+      }
+
       if (state.tokens.next.id !== ",") {
         break;
       }
@@ -3662,7 +3453,10 @@ var JSHINT = (function() {
     if (isStatement) {
       // BindingIdentifier
       this.name = identifier();
-      addlabel(this.name, { type: "unused", token: state.tokens.curr });
+
+      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();
@@ -3822,12 +3616,9 @@ var JSHINT = (function() {
       warning("W025");
     }
 
-    // check if a identifier with the same name is already defined
-    // in the blockscope as a const
-    if (state.funct["(blockscope)"].labeltype(i) === "const") {
-      warning("E011", null, i);
-    }
-    addlabel(i, { type: "unction", token: state.tokens.curr });
+    state.funct["(scope)"].addlabel(i, {
+      type: "function",
+      token: state.tokens.curr });
 
     doFunction({
       name: i,
@@ -3854,14 +3645,11 @@ var JSHINT = (function() {
     var i = optionalidentifier();
     var fn = doFunction({ name: i, type: generator ? "generator" : null });
 
-    function isVariable(name) { return name[0] !== "("; }
-    function isLocal(name) { return fn[name] === "var"; }
-
     if (!state.option.loopfunc && state.funct["(loopage)"]) {
       // If the function we just parsed accesses any non-local variables
       // trigger a warning. Otherwise, the function is safe even within
       // a loop.
-      if (_.some(fn, function(val, name) { return isVariable(name) && !isLocal(name); })) {
+      if (fn["(isCapturing)"]) {
         warning("W083");
       }
     }
@@ -3916,23 +3704,21 @@ var JSHINT = (function() {
     var b;
 
     function doCatch() {
-      var oldScope = scope;
       var e;
 
       advance("catch");
       advance("(");
 
-      scope = Object.create(oldScope);
-
-      e = state.tokens.next.value;
-      if (state.tokens.next.type !== "(identifier)") {
+      e = state.tokens.next;
+      if (e.type !== "(identifier)") {
+        warning("E030", state.tokens.next, e.value);
         e = null;
-        warning("E030", state.tokens.next, e);
+      } else {
+        // only advance if we have an identifier so we can continue parsing in the most common error - that no param is given.
+        advance();
       }
 
-      advance();
-
-      state.funct = functor("(catch)", state.tokens.next, scope, {
+      state.funct = functor("(catch)", state.tokens.next, {
         "(context)"  : state.funct,
         "(breakage)" : state.funct["(breakage)"],
         "(loopage)"  : state.funct["(loopage)"],
@@ -3940,9 +3726,7 @@ var JSHINT = (function() {
         "(catch)"    : true
       });
 
-      if (e) {
-        addlabel(e, { type: "exception" });
-      }
+      state.funct["(scope)"].stackParams(e ? [ { id: e.value, token: e, type: "exception" }] : []);
 
       if (state.tokens.next.value === "if") {
         if (!state.inMoz()) {
@@ -3954,12 +3738,11 @@ var JSHINT = (function() {
 
       advance(")");
 
-      state.tokens.curr.funct = state.funct;
       functions.push(state.funct);
 
       block(false);
 
-      scope = oldScope;
+      state.funct["(scope)"].unstack();
 
       state.funct["(last)"] = state.tokens.curr.line;
       state.funct["(lastcharacter)"] = state.tokens.curr.character;
@@ -4221,7 +4004,7 @@ var JSHINT = (function() {
         advance(state.tokens.next.id);
         // create a new block scope
         letscope = true;
-        state.funct["(blockscope)"].stack();
+        state.funct["(scope)"].stack();
         state.tokens.curr.fud({ prefix: true });
       } else {
         // Parse as a var statement, with implied bindings. Ignore errors if an error
@@ -4280,7 +4063,7 @@ var JSHINT = (function() {
           advance("let");
           // create a new block scope
           letscope = true;
-          state.funct["(blockscope)"].stack();
+          state.funct["(scope)"].stack();
           state.tokens.curr.fud();
         } else {
           for (;;) {
@@ -4319,7 +4102,7 @@ var JSHINT = (function() {
     }
     // unstack loop blockscope
     if (letscope) {
-      state.funct["(blockscope)"].unstack();
+      state.funct["(scope)"].unstack();
     }
     return this;
   }).labelled = true;
@@ -4336,10 +4119,8 @@ var JSHINT = (function() {
 
     if (state.tokens.next.id !== ";" && !state.tokens.next.reach) {
       if (state.tokens.curr.line === startLine(state.tokens.next)) {
-        if (state.funct[v] !== "label") {
+        if (!state.funct["(scope)"].funct.hasBreakLabel(v)) {
           warning("W090", state.tokens.next, v);
-        } else if (scope[v] !== state.funct) {
-          warning("W091", state.tokens.next, v);
         }
         this.first = state.tokens.next;
         advance();
@@ -4363,10 +4144,8 @@ var JSHINT = (function() {
 
     if (state.tokens.next.id !== ";" && !state.tokens.next.reach) {
       if (state.tokens.curr.line === startLine(state.tokens.next)) {
-        if (state.funct[v] !== "label") {
+        if (!state.funct["(scope)"].funct.hasBreakLabel(v)) {
           warning("W090", state.tokens.next, v);
-        } else if (scope[v] !== state.funct) {
-          warning("W091", state.tokens.next, v);
         }
         this.first = state.tokens.next;
         advance();
@@ -4472,7 +4251,10 @@ var JSHINT = (function() {
     if (state.tokens.next.identifier) {
       // ImportClause :: ImportedDefaultBinding
       this.name = identifier();
-      addlabel(this.name, { type: "unused", token: state.tokens.curr });
+      state.funct["(scope)"].addlabel(this.name, {
+        type: "var",
+        token: state.tokens.curr });
+
       if (state.tokens.next.value === ",") {
         // ImportClause :: ImportedDefaultBinding , NameSpaceImport
         // ImportClause :: ImportedDefaultBinding , NamedImports
@@ -4494,7 +4276,9 @@ var JSHINT = (function() {
       advance("as");
       if (state.tokens.next.identifier) {
         this.name = identifier();
-        addlabel(this.name, { type: "unused", token: state.tokens.curr });
+        state.funct["(scope)"].addlabel(this.name, {
+          type: "var",
+          token: state.tokens.curr });
       }
     } else {
       // ImportClause :: NamedImports
@@ -4515,7 +4299,10 @@ var JSHINT = (function() {
           advance("as");
           importName = identifier();
         }
-        addlabel(importName, { type: "unused", token: state.tokens.curr });
+
+        state.funct["(scope)"].addlabel(importName, {
+          type: "var",
+          token: state.tokens.curr });
 
         if (state.tokens.next.value === ",") {
           advance(",");
@@ -4545,7 +4332,7 @@ var JSHINT = (function() {
       ok = false;
     }
 
-    if (!state.funct["(global)"] || !state.funct["(blockscope)"].atTop()) {
+    if (!state.funct["(global)"] || !state.funct["(scope)"].atTop()) {
       error("E053", state.tokens.curr);
       ok = false;
     }
@@ -4577,9 +4364,11 @@ var JSHINT = (function() {
         identifier = token.value;
       }
 
-      addlabel(identifier, {
-        type: "function", token: token
-      });
+      state.funct["(scope)"].addlabel(identifier, {
+        type: "function",
+        token: token });
+
+      state.funct["(scope)"].setExported(identifier, token);
 
       return this;
     }
@@ -4594,7 +4383,6 @@ var JSHINT = (function() {
         }
         advance();
 
-        state.tokens.curr.exported = ok;
         exportedTokens.push(state.tokens.curr);
 
         if (state.tokens.next.value === "as") {
@@ -4621,11 +4409,7 @@ var JSHINT = (function() {
         advance("(string)");
       } else if (ok) {
         exportedTokens.forEach(function(token) {
-          if (!state.funct[token.value]) {
-            isundef(state.funct, "W117", token, token.value);
-          }
-          exported[token.value] = true;
-          state.funct["(blockscope)"].setExported(token.value);
+          state.funct["(scope)"].setExported(token.value, token);
         });
       }
       return this;
@@ -4647,15 +4431,13 @@ var JSHINT = (function() {
       // ExportDeclaration :: export Declaration
       this.block = true;
       advance("function");
-      exported[state.tokens.next.value] = ok;
-      state.tokens.next.exported = true;
+      state.funct["(scope)"].setExported(state.tokens.next.value, state.tokens.next);
       state.syntax["function"].fud();
     } else if (state.tokens.next.id === "class") {
       // ExportDeclaration :: export Declaration
       this.block = true;
       advance("class");
-      exported[state.tokens.next.value] = ok;
-      state.tokens.next.exported = true;
+      state.funct["(scope)"].setExported(state.tokens.next.value, state.tokens.next);
       state.syntax["class"].fud();
     } else {
       error("E024", state.tokens.next, state.tokens.next.value);
@@ -4872,7 +4654,7 @@ var JSHINT = (function() {
             if (v.unused)
               warning("W098", v.token, v.raw_text || v.value);
             if (v.undef)
-              isundef(v.state.funct, "W117", v.token, v.value);
+              state.funct["(scope)"].block.use(v.value, v.token);
           });
           _carrays.splice(-1, 1);
           _current = _carrays[_carrays.length - 1];
@@ -4912,14 +4694,14 @@ var JSHINT = (function() {
             return true;
           // When we are in the "generate" state of the list comp,
           } else if (_current && _current.mode === "generate") {
-            isundef(state.funct, "W117", state.tokens.curr, v);
+            state.funct["(scope)"].block.use(v, state.tokens.curr);
             return true;
           // When we are in "filter" state,
           } else if (_current && _current.mode === "filter") {
             // we check whether current variable has been declared
             if (use(v)) {
               // if not we warn about it
-              isundef(state.funct, "W117", state.tokens.curr, v);
+              state.funct["(scope)"].block.use(v, state.tokens.curr);
             }
             return true;
           }
@@ -5014,141 +4796,6 @@ var JSHINT = (function() {
     }
   }
 
-  var warnUnused = function(name, tkn, type, unused_opt) {
-    var line = tkn.line;
-    var chr  = tkn.from;
-    var raw_name = tkn.raw_text || name;
-
-    if (unused_opt === undefined) {
-      unused_opt = state.option.unused;
-    }
-
-    if (unused_opt === true) {
-      unused_opt = "last-param";
-    }
-
-    var warnable_types = {
-      "vars": ["var"],
-      "last-param": ["var", "param"],
-      "strict": ["var", "param", "last-param"]
-    };
-
-    if (unused_opt) {
-      if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) {
-        if (!tkn.exported) {
-          warningAt("W098", line, chr, raw_name);
-        }
-      }
-    }
-
-    unuseds.push({
-      name: name,
-      line: line,
-      character: chr
-    });
-  };
-
-  var blockScope = function() {
-    var _current = {};
-    var _variables = [_current];
-
-    function _checkBlockLabels() {
-      for (var t in _current) {
-        var label = _current[t],
-            labelType = label["(type)"];
-        if (labelType === "unused" || (labelType === "const" && label["(unused)"])) {
-          if (state.option.unused) {
-            var tkn = _current[t]["(token)"];
-            // Don't report exported labels as unused
-            if (tkn.exported) {
-              continue;
-            }
-
-            warnUnused(t, tkn, "var");
-          }
-        }
-      }
-    }
-
-    function _getLabel(l) {
-      for (var i = _variables.length - 1 ; i >= 0; --i) {
-        if (_.has(_variables[i], l) && !_variables[i][l]["(shadowed)"]) {
-          return _variables[i];
-        }
-      }
-    }
-
-    return {
-      stack: function() {
-        _current = {};
-        _variables.push(_current);
-      },
-
-      unstack: function() {
-        _checkBlockLabels();
-        _variables.splice(_variables.length - 1, 1);
-        _current = _.last(_variables);
-      },
-
-      getlabel: _getLabel,
-
-      labeltype: function(l) {
-        // returns a labels type or null if not present
-        var block = _getLabel(l);
-        if (block) {
-          return block[l]["(type)"];
-        }
-        return null;
-      },
-
-      shadow: function(name) {
-        for (var i = _variables.length - 1; i >= 0; i--) {
-          if (_.has(_variables[i], name)) {
-            _variables[i][name]["(shadowed)"] = true;
-          }
-        }
-      },
-
-      unshadow: function(name) {
-        for (var i = _variables.length - 1; i >= 0; i--) {
-          if (_.has(_variables[i], name)) {
-            _variables[i][name]["(shadowed)"] = false;
-          }
-        }
-      },
-
-      atTop: function() {
-        return _variables.length === 1;
-      },
-
-      setExported: function(id) {
-        if (state.funct["(blockscope)"].atTop()) {
-          var item = _current[id];
-          if (item && item["(token)"]) {
-            item["(token)"].exported = true;
-          }
-        }
-      },
-
-      current: {
-        labeltype: function(t) {
-          if (_current[t]) {
-            return _current[t]["(type)"];
-          }
-          return null;
-        },
-
-        has: function(t) {
-          return _.has(_current, t);
-        },
-
-        add: function(t, type, tok) {
-          _current[t] = { "(type)" : type, "(token)": tok, "(shadowed)": false, "(unused)": true };
-        }
-      }
-    };
-  };
-
   var escapeRegex = function(str) {
     return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
   };
@@ -5180,7 +4827,7 @@ var JSHINT = (function() {
     combine(predefined, g || {});
 
     declared = Object.create(null);
-    exported = Object.create(null);
+    var exported = Object.create(null); // Variables that live outside the current file
 
     function each(obj, cb) {
       if (!obj)
@@ -5240,12 +4887,19 @@ var JSHINT = (function() {
     state.option.maxerr = state.option.maxerr || 50;
 
     indent = 1;
-    global = Object.create(predefined);
-    scope = global;
 
-    state.funct = functor("(global)", null, scope, {
+    var scopeManagerInst = scopeManager(state, predefined, exported, declared);
+    scopeManagerInst.on("warning", function(ev) {
+      warning.apply(null, [ ev.code, ev.token].concat(ev.data));
+    });
+
+    scopeManagerInst.on("error", function(ev) {
+      error.apply(null, [ ev.code, ev.token ].concat(ev.data));
+    });
+
+    state.funct = functor("(global)", null, {
       "(global)"    : true,
-      "(blockscope)": blockScope(),
+      "(scope)"     : scopeManagerInst,
       "(comparray)" : arrayComprehension(),
       "(metrics)"   : createMetrics(state.tokens.next)
     });
@@ -5255,10 +4909,8 @@ var JSHINT = (function() {
     stack = null;
     member = {};
     membersOnly = null;
-    implied = {};
     inblock = false;
     lookahead = [];
-    unuseds = [];
 
     if (!isString(s) && !Array.isArray(s)) {
       errorAt("E004", 0);
@@ -5391,123 +5043,7 @@ var JSHINT = (function() {
         quit("E041", state.tokens.curr.line);
       }
 
-      state.funct["(blockscope)"].unstack();
-
-      var markDefined = function(name, context) {
-        do {
-          if (typeof context[name] === "string") {
-            // JSHINT marks unused variables as 'unused' and
-            // unused function declaration as 'unction'. This
-            // code changes such instances back 'var' and
-            // 'closure' so that the code in JSHINT.data()
-            // doesn't think they're unused.
-
-            if (context[name] === "unused")
-              context[name] = "var";
-            else if (context[name] === "unction")
-              context[name] = "closure";
-
-            return true;
-          }
-
-          context = context["(context)"];
-        } while (context);
-
-        return false;
-      };
-
-      var clearImplied = function(name, line) {
-        if (!implied[name])
-          return;
-
-        var newImplied = [];
-        for (var i = 0; i < implied[name].length; i += 1) {
-          if (implied[name][i] !== line)
-            newImplied.push(implied[name][i]);
-        }
-
-        if (newImplied.length === 0)
-          delete implied[name];
-        else
-          implied[name] = newImplied;
-      };
-
-      var checkUnused = function(func, key) {
-        var type = func[key];
-        var tkn = func["(tokens)"][key];
-
-        if (key.charAt(0) === "(")
-          return;
-
-        if (type !== "unused" && type !== "unction")
-          return;
-
-        // Params are checked separately from other variables.
-        if (func["(params)"] && func["(params)"].indexOf(key) !== -1)
-          return;
-
-        // Variable is in global scope and defined as exported.
-        if (func["(global)"] && _.has(exported, key))
-          return;
-
-        warnUnused(key, tkn, "var");
-      };
-
-      // Check queued 'x is not defined' instances to see if they're still undefined.
-      for (i = 0; i < JSHINT.undefs.length; i += 1) {
-        k = JSHINT.undefs[i].slice(0);
-
-        if (markDefined(k[2].value, k[0]) || k[2].forgiveUndef) {
-          clearImplied(k[2].value, k[2].line);
-        } else if (state.option.undef) {
-          warning.apply(warning, k.slice(1));
-        }
-      }
-
-      functions.forEach(function(func) {
-        if (func["(unusedOption)"] === false) {
-          return;
-        }
-
-        for (var key in func) {
-          if (_.has(func, key)) {
-            checkUnused(func, key);
-          }
-        }
-
-        if (!func["(params)"])
-          return;
-
-        var params = func["(params)"].slice();
-        var param  = params.pop();
-        var type, unused_opt;
-
-        while (param) {
-          type = func[param];
-          unused_opt = func["(unusedOption)"] || state.option.unused;
-          unused_opt = unused_opt === true ? "last-param" : unused_opt;
-
-          // 'undefined' is a special case for (function(window, undefined) { ... })();
-          // patterns.
-
-          if (param === "undefined")
-            return;
-
-          if (type === "unused" || type === "unction") {
-            warnUnused(param, func["(tokens)"][param], "param", func["(unusedOption)"]);
-          } else if (unused_opt === "last-param") {
-            return;
-          }
-
-          param = params.pop();
-        }
-      });
-
-      for (var key in declared) {
-        if (_.has(declared, key) && !_.has(global, key) && !_.has(exported, key)) {
-          warnUnused(key, declared[key], "var");
-        }
-      }
+      state.funct["(scope)"].unstack();
 
     } catch (err) {
       if (err && err.name === "JSHintError") {
@@ -5554,7 +5090,6 @@ var JSHINT = (function() {
       options: state.option
     };
 
-    var implieds = [];
     var members = [];
     var fu, f, i, j, n, globals;
 
@@ -5566,24 +5101,16 @@ var JSHINT = (function() {
       data.json = true;
     }
 
-    for (n in implied) {
-      if (_.has(implied, n)) {
-        implieds.push({
-          name: n,
-          line: implied[n]
-        });
-      }
-    }
-
-    if (implieds.length > 0) {
-      data.implieds = implieds;
+    var impliedGlobals = state.funct["(scope)"].getImpliedGlobals();
+    if (impliedGlobals.length > 0) {
+      data.implieds = impliedGlobals;
     }
 
     if (urls.length > 0) {
       data.urls = urls;
     }
 
-    globals = Object.keys(scope);
+    globals = state.funct["(scope)"].getUsedOrDefinedGlobals();
     if (globals.length > 0) {
       data.globals = globals;
     }
@@ -5618,6 +5145,7 @@ var JSHINT = (function() {
       data.functions.push(fu);
     }
 
+    var unuseds = state.funct["(scope)"].getUnuseds();
     if (unuseds.length > 0) {
       data.unused = unuseds;
     }
diff --git a/src/messages.js b/src/messages.js
index 1f33d1bb..4baa2510 100644
--- a/src/messages.js
+++ b/src/messages.js
@@ -20,7 +20,7 @@ var errors = {
   E010: "'with' is not allowed in strict mode.",
 
   // Constants
-  E011: "const '{a}' has already been declared.",
+  E011: "'{a}' has already been declared.",
   E012: "const '{a}' is initialized to 'undefined'.",
   E013: "Attempting to override '{a}' which is a constant.",
 
@@ -70,7 +70,8 @@ var errors = {
   E052: "Unclosed template literal.",
   E053: "Export declaration must be in global scope.",
   E054: "Class properties must be methods. Expected '(' but instead saw '{a}'.",
-  E055: "The '{a}' option cannot be set after any executable code."
+  E055: "The '{a}' option cannot be set after any executable code.",
+  E056: "'{a}' was used before it was declared, which is illegal for '{b}' variables."
 };
 
 var warnings = {
@@ -168,7 +169,7 @@ var warnings = {
   W089: "The body of a for in should be wrapped in an if statement to filter " +
     "unwanted properties from the prototype.",
   W090: "'{a}' is not a statement label.",
-  W091: "'{a}' is out of scope.",
+  W091: null,
   W093: "Did you mean to return a conditional instead of an assignment?",
   W094: "Unexpected comma.",
   W095: "Expected a string and instead saw {a}.",
diff --git a/src/scope-manager.js b/src/scope-manager.js
new file mode 100644
index 00000000..79f57447
--- /dev/null
+++ b/src/scope-manager.js
@@ -0,0 +1,708 @@
+"use strict";
+
+var _      = require("lodash");
+var events = require("events");
+
+/**
+ * Creates a scope manager that handles variables and labels, storing usages
+ * and resolving when variables are used and undefined
+ */
+var scopeManager = function(state, predefined, exported, declared) {
+  function _newScope() {
+    return {
+      "(labels)": Object.create(null),
+      "(usages)": Object.create(null),
+      "(breakLabels)": Object.create(null),
+      "(parent)": _current
+    };
+  }
+
+  var _current = _newScope();
+  _current["(predefined)"] = predefined;
+  _current["(global)"] = true;
+
+  var _currentFunct = _current; // this is the block after the params = function
+  var _scopeStack = [_current];
+
+  var usedPredefinedAndGlobals = Object.create(null);
+  var impliedGlobals = {};
+  var unuseds = [];
+  var emitter = new events.EventEmitter();
+
+  function warning(code, token) {
+    emitter.emit("warning", {
+      code: code,
+      token: token,
+      data: _.slice(arguments, 2)
+    });
+  }
+
+  function error(code, token) {
+    emitter.emit("warning", {
+      code: code,
+      token: token,
+      data: _.slice(arguments, 2)
+    });
+  }
+
+  function _addUsage(labelName, token) {
+    if (token) {
+      token["(function)"] = _currentFunct;
+    }
+    if (!_.has(_current["(usages)"], labelName)) {
+      _current["(usages)"][labelName] = {
+        "(modified)": [],
+        "(reassigned)": [],
+        "(tokens)": token ? [token] : []
+      };
+    } else if (token) {
+      _current["(usages)"][labelName]["(tokens)"].push(token);
+    }
+  }
+
+  var exportedLabels = Object.keys(exported);
+  for (var i = 0; i < exportedLabels.length; i++) {
+    _addUsage(exportedLabels[i]);
+  }
+
+  var _getUnusedOption = function(unused_opt) {
+    if (unused_opt === undefined) {
+      unused_opt = state.option.unused;
+    }
+
+    if (unused_opt === true) {
+      unused_opt = "last-param";
+    }
+
+    return unused_opt;
+  };
+
+  var _warnUnused = function(name, tkn, type, unused_opt) {
+    var line = tkn.line;
+    var chr  = tkn.from;
+    var raw_name = tkn.raw_text || name;
+
+    unused_opt = _getUnusedOption(unused_opt);
+
+    var warnable_types = {
+      "vars": ["var"],
+      "last-param": ["var", "param"],
+      "strict": ["var", "param", "last-param"]
+    };
+
+    if (unused_opt) {
+      if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) {
+        warning("W098", { line: line, from: chr }, raw_name);
+      }
+    }
+
+    // inconsistent - see gh-1894
+    if (unused_opt || type === "var") {
+      unuseds.push({
+        name: name,
+        line: line,
+        character: chr
+      });
+    }
+  };
+
+  /**
+   * Checks the current scope for unused identifiers
+   */
+  function _checkForUnused() {
+    // function params are handled specially
+    if (_current["(isParams)"] === "function") {
+      _checkParams();
+      return;
+    }
+    var curentLabels = _current["(labels)"];
+    for (var labelName in curentLabels) {
+      if (_.has(curentLabels, labelName)) {
+        if (curentLabels[labelName]["(type)"] !== "exception" &&
+          curentLabels[labelName]["(unused)"]) {
+          _warnUnused(labelName, curentLabels[labelName]["(token)"], "var");
+        }
+      }
+    }
+  }
+
+  /**
+   * Checks the current scope for unused parameters
+   * Must be called in a function parameter scope
+   */
+  function _checkParams() {
+    var params = _current["(params)"];
+    var param = params.pop();
+    var unused_opt;
+
+    while (param) {
+      var label = _current["(labels)"][param];
+
+      unused_opt = _getUnusedOption(state.funct["(unusedOption)"]);
+
+      // 'undefined' is a special case for (function(window, undefined) { ... })();
+      // patterns.
+      if (param === "undefined")
+        return;
+
+      if (label["(unused)"]) {
+        _warnUnused(param, label["(token)"], "param", state.funct["(unusedOption)"]);
+      } else if (unused_opt === "last-param") {
+        return;
+      }
+
+      param = params.pop();
+    }
+  }
+
+  /**
+   * Finds the relevant label's scope, searching from nearest outwards
+   * @returns {Object} the scope the label was found in
+   */
+  function _getLabel(labelName) {
+    for (var i = _scopeStack.length - 1 ; i >= 0; --i) {
+      var scopeLabels = _scopeStack[i]["(labels)"];
+      if (_.has(scopeLabels, labelName)) {
+        return scopeLabels;
+      }
+    }
+  }
+
+  function usedSoFarInCurrentFunction(labelName) {
+    // used so far in this whole function and any sub functions
+    for (var i = _scopeStack.length - 1; i >= 0; i--) {
+      var current = _scopeStack[i];
+      if (_.has(current["(usages)"], labelName)) {
+        return current["(usages)"][labelName];
+      }
+      if (current === _currentFunct) {
+        break;
+      }
+    }
+    return false;
+  }
+
+  function _checkOuterShadow(labelName, token) {
+
+    // only check if shadow is outer
+    if (state.option.shadow !== "outer") {
+      return;
+    }
+
+    var isGlobal = _currentFunct["(global)"],
+      isNewFunction = _current["(isParams)"] === "function";
+
+    var outsideCurrentFunction = !isGlobal;
+    for (var i = 0; i < _scopeStack.length; i++) {
+      var stackItem = _scopeStack[i];
+
+      if (!isNewFunction && _scopeStack[i + 1] === _currentFunct) {
+        outsideCurrentFunction = false;
+      }
+      if (outsideCurrentFunction && _.has(stackItem["(labels)"], labelName)) {
+        warning("W123", token, labelName);
+        //break;
+      }
+      if (_.has(stackItem["(breakLabels)"], labelName)) {
+        warning("W123", token, labelName);
+      }
+    }
+  }
+
+  function _latedefWarning(type, labelName, token) {
+    if (state.option.latedef) {
+      // if either latedef is strict and this is a function
+      //    or this is not a function
+      if ((state.option.latedef === true && type === "function") ||
+        type !== "function") {
+        warning("W003", token, labelName);
+      }
+    }
+  }
+
+  var scopeManagerInst = {
+
+    on: function(names, listener) {
+      names.split(" ").forEach(function(name) {
+        emitter.on(name, listener);
+      });
+    },
+
+    isPredefined: function(labelName) {
+      return !this.has(labelName) && _.has(_scopeStack[0]["(predefined)"], labelName);
+    },
+
+    /**
+     * Tell the manager we are entering a new block of code
+     */
+    stack: function() {
+      var previousScope = _current;
+      _current = _newScope();
+
+      if (previousScope["(isParams)"] === "function") {
+
+        _current["(isFunc)"] = true;
+        _current["(context)"] = _currentFunct;
+        _currentFunct = _current;
+      }
+      _scopeStack.push(_current);
+    },
+
+    unstack: function() {
+
+      var subScope = _scopeStack.length > 1 ? _scopeStack[_scopeStack.length - 2] : null;
+      var isUnstackingFunction = _current === _currentFunct;
+
+      var i, j;
+      var currentUsages = _current["(usages)"];
+      var currentLabels = _current["(labels)"];
+      var usedLabelNameList = Object.keys(currentUsages);
+      for (i = 0; i < usedLabelNameList.length; i++) {
+        var usedLabelName = usedLabelNameList[i];
+
+        var usage = currentUsages[usedLabelName];
+        var usedLabel = _.has(currentLabels, usedLabelName) && currentLabels[usedLabelName];
+        if (usedLabel) {
+
+          if (usedLabel["(useOutsideOfScope)"] && !state.option.funcscope) {
+            var usedTokens = usage["(tokens)"];
+            if (usedTokens) {
+              for (j = 0; j < usedTokens.length; j++) {
+                // Keep the consistency of https://github.com/jshint/jshint/issues/2409
+                if (usedLabel["(function)"] === usedTokens[j]["(function)"]) {
+                  error("W038", usedTokens[j], usedLabelName);
+                }
+              }
+            }
+          }
+
+          // mark the label used
+          _current["(labels)"][usedLabelName]["(unused)"] = false;
+
+          // check for modifying a const
+          if (usedLabel["(type)"] === "const" && usage["(modified)"]) {
+            for (j = 0; j < usage["(modified)"].length; j++) {
+              error("E013", usage["(modified)"][j], usedLabelName);
+            }
+          }
+
+          // check for re-assigning a function declaration
+          if (usedLabel["(type)"] === "function" && usage["(reassigned)"]) {
+            for (j = 0; j < usage["(reassigned)"].length; j++) {
+              error("W021", usage["(reassigned)"][j], usedLabelName);
+            }
+          }
+          continue;
+        }
+
+        if (isUnstackingFunction) {
+          state.funct["(isCapturing)"] = true;
+        }
+
+        if (subScope) {
+          // not exiting the global scope, so copy the usage down in case its an out of scope usage
+          if (!_.has(subScope["(usages)"], usedLabelName)) {
+            subScope["(usages)"][usedLabelName] = usage;
+            if (isUnstackingFunction) {
+              subScope["(usages)"][usedLabelName]["(onlyUsedSubFunction)"] = true;
+            }
+          } else {
+            var subScopeUsage = subScope["(usages)"][usedLabelName];
+            subScopeUsage["(modified)"] = subScopeUsage["(modified)"].concat(usage["(modified)"]);
+            subScopeUsage["(tokens)"] = subScopeUsage["(tokens)"].concat(usage["(tokens)"]);
+            subScopeUsage["(reassigned)"] =
+              subScopeUsage["(reassigned)"].concat(usage["(reassigned)"]);
+            subScopeUsage["(onlyUsedSubFunction)"] = false;
+          }
+        } else {
+          // this is exiting global scope, so we finalise everything here - we are at the end of the file
+          if (_.has(_current["(predefined)"], usedLabelName)) {
+
+            // remove the declared token, so we know it is used
+            delete declared[usedLabelName];
+
+            // note it as used so it can be reported
+            usedPredefinedAndGlobals[usedLabelName] = true;
+
+            // check for re-assigning a read-only (set to false) predefined
+            if (_current["(predefined)"][usedLabelName] === false && usage["(reassigned)"]) {
+              for (j = 0; j < usage["(reassigned)"].length; j++) {
+                warning("W020", usage["(reassigned)"][j]);
+              }
+            }
+          }
+          else {
+            // label usage is not predefined and we have not found a declaration
+            // so report as undeclared
+            if (usage["(tokens)"]) {
+              for (j = 0; j < usage["(tokens)"].length; j++) {
+                var undefinedToken = usage["(tokens)"][j];
+                // if its not a forgiven undefined (e.g. typof x)
+                if (!undefinedToken.forgiveUndef) {
+                  // if undef is on and undef was on when the token was defined
+                  if (state.option.undef && !undefinedToken.ignoreUndef) {
+                    warning("W117", undefinedToken, usedLabelName);
+                  }
+                  if (_.has(impliedGlobals, usedLabelName)) {
+                    impliedGlobals[usedLabelName].line.push(undefinedToken.line);
+                  } else {
+                    impliedGlobals[usedLabelName] = {
+                      name: usedLabelName,
+                      line: [undefinedToken.line]
+                    };
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+
+      // if exiting the global scope, we can warn about declared globals that haven't been used yet
+      if (!subScope) {
+        Object.keys(declared)
+          .forEach(function(labelNotUsed) {
+            _warnUnused(labelNotUsed, declared[labelNotUsed], "var");
+          });
+      }
+
+      // if we have a sub scope we can copy too and we are still within the function boundary
+      if (subScope && !isUnstackingFunction && _current["(isParams)"] !== "function") {
+        var labelNames = Object.keys(currentLabels);
+        for (i = 0; i < labelNames.length; i++) {
+
+          var defLabelName = labelNames[i];
+
+          // if its function scoped and
+          // not already defined (caught with shadow, shouldn't also trigger out of scope)
+          if (!currentLabels[defLabelName]["(blockscoped)"] &&
+            currentLabels[defLabelName]["(type)"] !== "exception" &&
+            !this.funct.has(defLabelName, { excludeCurrent: true })) {
+            subScope["(labels)"][defLabelName] = currentLabels[defLabelName];
+            // we do not warn about out of scope usages in the global scope
+            if (!_currentFunct["(global)"]) {
+              subScope["(labels)"][defLabelName]["(useOutsideOfScope)"] = true;
+            }
+            delete currentLabels[defLabelName];
+          }
+        }
+      }
+
+      _checkForUnused();
+
+      _scopeStack.pop();
+      if (isUnstackingFunction) {
+        _currentFunct = _scopeStack[_.findLastIndex(_scopeStack, function(scope) {
+          // if function or if global (which is at the bottom so it will only return true if we call back)
+          return scope["(isFunc)"] || scope["(global)"];
+        })];
+      }
+
+      _current = subScope;
+    },
+
+    /**
+     * Tell the manager we are entering a new scope with parameters
+     * Call stack after all parameters have been added
+     * @param isFunction Whether the scope is a function or not
+     */
+    stackParams: function(params, isFunction) {
+      _current = _newScope();
+      _current["(isParams)"] = isFunction ? "function" : true;
+      _current["(params)"] = [];
+
+      _scopeStack.push(_current);
+      var functionScope = this.funct;
+
+      _.each(params, function(param) {
+
+        var type = param.type || "param";
+
+        if (type === "exception") {
+          // if defined in the current function
+          var previouslyDefinedLabelType = functionScope.labeltype(param.id);
+          if (previouslyDefinedLabelType && previouslyDefinedLabelType !== "exception") {
+            // and has not been used yet in the current function scope
+            if (!state.option.node) {
+              warning("W002", state.tokens.next, param.id);
+            }
+          }
+        }
+
+        _checkOuterShadow(param.id, param.token, type);
+
+        _current["(labels)"][param.id] = {
+          "(type)" : type,
+          "(token)": param.token,
+          "(unused)": true };
+        _current["(params)"].push(param.id);
+      });
+    },
+
+    getUsedOrDefinedGlobals: function() {
+      return Object.keys(usedPredefinedAndGlobals);
+    },
+
+    /**
+     * Gets an array of implied globals
+     * @returns {Array.<{ name: string, line: Array.<number>}>}
+     */
+    getImpliedGlobals: function() {
+      return _.values(impliedGlobals);
+    },
+
+    /**
+     * Returns a list of unused variables
+     * @returns {Array}
+     */
+    getUnuseds: function() {
+      return unuseds;
+    },
+
+    has: function(labelName) {
+      return Boolean(_getLabel(labelName));
+    },
+
+    labeltype: function(labelName) {
+      // returns a labels type or null if not present
+      var scopeLabels = _getLabel(labelName);
+      if (scopeLabels) {
+        return scopeLabels[labelName]["(type)"];
+      }
+      return null;
+    },
+
+    atTop: function() {
+      return _scopeStack.length === 1;
+    },
+
+    /**
+     * for the exported options, indicating a variable is used outside the file
+     */
+    addExported: _addUsage,
+
+    /**
+     * Mark an indentifier as es6 module exported
+     */
+    setExported: function(labelName, token) {
+      this.block.use(labelName, token);
+    },
+
+    /**
+     * 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
+     */
+    addlabel: function(labelName, opts) {
+
+      var type  = opts.type;
+      var token = opts.token;
+      var isblockscoped = type === "let" || type === "const";
+
+      // late definition check
+      // this is for when a variable we thought was a closure turns out to not be a closure.
+      if (isblockscoped) {
+
+        // is used in TDZ
+        if (_.has(_current["(usages)"], labelName)) {
+          var usage = _current["(usages)"][labelName];
+          // if its in a sub function it is not necessarily an error, just latedef
+          if (usage["(onlyUsedSubFunction)"]) {
+            _latedefWarning(type, labelName, token);
+          } else {
+            // this is a clear illegal usage for block scoped variables
+            warning("E056", token, labelName, type);
+          }
+        }
+      } else {
+        if (usedSoFarInCurrentFunction(labelName)) {
+          _latedefWarning(type, labelName, token);
+        }
+      }
+
+      // outer shadow check (inner is only on non-block scoped)
+      _checkOuterShadow(labelName, token, type);
+
+      // if the identifier is blockscoped (a let or a const),
+      // add it only to the current blockscope
+      if (isblockscoped) {
+
+        var declaredInCurrentScope = _.has(_current["(labels)"], labelName);
+        // for block scoped variables, params are seen in the current scope, so also
+        // throw an error. So if we are in the function scope (not an inner scope)
+        // then check params too
+        if (!declaredInCurrentScope && _current === _currentFunct && !_current["(global)"]) {
+          declaredInCurrentScope = _.has(_currentFunct["(parent)"]["(labels)"], labelName);
+        }
+        // if this scope has the variable defined, its a re-definition error
+        if (declaredInCurrentScope) {
+          warning("E011", token, labelName);
+        }
+        else if (state.option.shadow === "outer") {
+
+          // if shadow is outer, for block scope we want to detect any shadowing within this function
+          if (scopeManagerInst.funct.has(labelName)) {
+            warning("W004", token, labelName);
+          }
+        }
+
+        scopeManagerInst.block.add(labelName, type, token, true);
+      } else {
+
+        // defining with a var or a function when a block scope variable of the same name
+        // is in scope is an error
+        if (scopeManagerInst.funct.has(labelName, { onlyBlockscoped: true })) {
+          warning("E011", token, labelName);
+        } else if (state.option.shadow !== true) {
+          // now since we didn't get any block scope variables, test for var/function
+          // shadowing
+          if (scopeManagerInst.funct.has(labelName)) {
+
+            // see https://github.com/jshint/jshint/issues/2400
+            if (!_currentFunct["(global)"]) {
+              warning("W004", token, labelName);
+            }
+          }
+        }
+
+        scopeManagerInst.funct.add(labelName, type, token, true);
+
+        if (state.funct["(global)"]) {
+          usedPredefinedAndGlobals[labelName] = true;
+        }
+      }
+    },
+
+    funct: {
+      /**
+       * Returns the label type given certain options
+       * @param labelName
+       * @param {Object=} options
+       * @param {Boolean=} options.onlyBlockscoped - only include block scoped labels
+       * @param {Boolean=} options.excludeParams - exclude the param scope
+       * @param {Boolean=} options.excludeCurrent - exclude the current scope
+       * @returns {String}
+       */
+      labeltype: function(labelName, options) {
+        var onlyBlockscoped = options && options.onlyBlockscoped;
+        var excludeParams = options && options.excludeParams;
+        var currentScopeIndex = _scopeStack.length - (options && options.excludeCurrent ? 2 : 1);
+        for (var i = currentScopeIndex; i >= 0; i--) {
+          var current = _scopeStack[i];
+          if (_.has(current["(labels)"], labelName) &&
+            (!onlyBlockscoped || current["(labels)"][labelName]["(blockscoped)"])) {
+            return current["(labels)"][labelName]["(type)"];
+          }
+          var scopeCheck = excludeParams ? _scopeStack[ i - 1 ] : current;
+          if (scopeCheck && scopeCheck["(isParams)"] === "function") {
+            return null;
+          }
+        }
+        return null;
+      },
+      /**
+       * Returns if a break label exists in the function scope
+       * @param {string} labelName
+       * @returns {boolean}
+       */
+      hasBreakLabel: function(labelName) {
+        for (var i = _scopeStack.length - 1; i >= 0; i--) {
+          var current = _scopeStack[i];
+
+          if (_.has(current["(breakLabels)"], labelName)) {
+            return true;
+          }
+          if (current["(isParams)"] === "function") {
+            return false;
+          }
+        }
+        return false;
+      },
+      /**
+       * Returns if the label is in the current function scope
+       * See state.funct.labelType for options
+       */
+      has: function(labelName, options) {
+        return Boolean(this.labeltype(labelName, options));
+      },
+
+      /**
+       * Adds a new function scoped variable
+       * see current.add for block scoped
+       */
+      add: function(labelName, type, tok, unused) {
+        _current["(labels)"][labelName] = {
+          "(type)" : type,
+          "(token)": tok,
+          "(blockscoped)": false,
+          "(function)": _currentFunct,
+          "(unused)": unused };
+      }
+    },
+
+    block: {
+      use: function(labelName, token) {
+
+        // if resolves to current function params, then do not store usage just resolve
+        // this is because function(a) { var a = a; } will resolve to the param, not
+        // to the unset var
+        // first check the param is used
+        var paramScope = _currentFunct["(parent)"];
+        if (paramScope && _.has(paramScope["(labels)"], labelName) &&
+          paramScope["(labels)"][labelName]["(type)"] === "param") {
+
+          // then check its not used
+          if (!scopeManagerInst.funct.has(labelName, { excludeParams: true })) {
+            paramScope["(labels)"][labelName]["(unused)"] = false;
+          }
+        }
+
+        if (token && (state.ignored.W117 || state.option.undef === false)) {
+          token.ignoreUndef = true;
+        }
+
+        _addUsage(labelName, token);
+      },
+
+      reassign: function(labelName, token) {
+
+        this.modify(labelName, token);
+
+        _current["(usages)"][labelName]["(reassigned)"].push(token);
+      },
+
+      modify: function(labelName, token) {
+
+        _current["(usages)"][labelName]["(modified)"].push(token);
+      },
+
+      /**
+       * Adds a new variable
+       */
+      add: function(labelName, type, tok, unused) {
+        _current["(labels)"][labelName] = {
+          "(type)" : type,
+          "(token)": tok,
+          "(blockscoped)": true,
+          "(unused)": unused };
+      },
+
+      addBreakLabel: function(labelName, opts) {
+        var token = opts.token;
+        if (state.option.shadow === "outer") {
+          if (scopeManagerInst.funct.has(labelName)) {
+            warning("W004", token, labelName);
+          } else {
+            _checkOuterShadow(labelName, token);
+          }
+        }
+        _current["(breakLabels)"][labelName] = token;
+      }
+    }
+  };
+  return scopeManagerInst;
+};
+
+module.exports = scopeManager;


Test patch:
diff --git a/tests/regression/thirdparty.js b/tests/regression/thirdparty.js
index f5cf927b..d9bd4cb1 100644
--- a/tests/regression/thirdparty.js
+++ b/tests/regression/thirdparty.js
@@ -88,6 +88,7 @@ exports.prototype_1_7 = function (test) {
     .addError(2989, "'tagName' used out of scope.")
     .addError(2989, "'tagName' used out of scope.")
     .addError(2990, "'tagName' used out of scope.")
+    .addError(3827, "'getOffsetParent' is a function.")
     .addError(3844, "'positionedOffset' is a function.")
     .addError(3860, "'cumulativeOffset' is a function.")
     .addError(4036, "'ret' is already defined.")
@@ -163,8 +164,6 @@ exports.lodash_0_6_1 = function (test) {
     .addError(1159, "'isArr' used out of scope.")
     .addError(1490, "Use '===' to compare with '0'.")
     .addError(1670, "Missing semicolon.")
-    .addError(2731, "'array' is already defined.")
-    .addError(2732, "'array' is a statement label.")
     .addError(3374, "Possible strict violation.")
     .addError(3377, "Missing '()' invoking a constructor.")
     .addError(3384, "Missing semicolon.")
@@ -212,18 +211,11 @@ exports.codemirror3 = function (test) {
   };
 
   TestRun(test)
-    .addError(1157, "'result' is defined but never used.")
     .addError(1342, "Value of 'e' may be overwritten in IE 8 and earlier.")
     .addError(1526, "Value of 'e' may be overwritten in IE 8 and earlier.")
-    .addError(1532, "'mX' is defined but never used.")
-    .addError(1532, "'mY' is defined but never used.")
     .addError(1533, "Value of 'e' may be overwritten in IE 8 and earlier.")
-    .addError(2218, "'state' is defined but never used.")
-    .addError(2427, "'style' is defined but never used.")
-    .addError(2696, "'target' is defined but never used.")
     .addError(3168, "'ok' is defined but never used.")
     .addError(4093, "Unnecessary semicolon.")
-    .addError(4277, "'range' is defined but never used.")
     .test(src, opt, { CodeMirror: true });
 
   test.done();
diff --git a/tests/unit/core.js b/tests/unit/core.js
index 4910e701..3a903bf7 100644
--- a/tests/unit/core.js
+++ b/tests/unit/core.js
@@ -22,7 +22,8 @@ exports.testCustomGlobals = function (test) {
   for (var i = 0, g; g = report.globals[i]; i += 1)
     dict[g] = true;
 
-  for (i = 0, g = null; g = custom[i]; i += 1)
+  var customKeys = Object.keys(custom);
+  for (i = 0, g = null; g = customKeys[i]; i += 1)
     test.ok(g in dict);
 
   // Regression test (GH-665)
@@ -50,6 +51,31 @@ exports.testUnusedDefinedGlobals = function (test) {
   test.done();
 };
 
+exports.testExportedDefinedGlobals = function (test) {
+  var src = ["/*global foo, bar */",
+    "export { bar, foo };"];
+
+  // Test should pass
+  TestRun(test).test(src, { esnext: true, unused: true }, {});
+
+  var report = JSHINT.data();
+  test.deepEqual(report.globals, ['bar', 'foo']);
+
+  test.done();
+};
+
+exports.testGlobalVarDeclarations = function (test) {
+  var src = "var a;";
+
+  // Test should pass
+  TestRun(test).test(src, { es3: true, node: true }, {});
+
+  var report = JSHINT.data();
+  test.deepEqual(report.globals, ['a']);
+
+  test.done();
+};
+
 exports.globalDeclarations = function (test) {
   var src = "exports = module.exports = function (test) {};";
 
@@ -71,7 +97,9 @@ exports.globalDeclarations = function (test) {
 exports.multilineGlobalDeclarations = function (test) {
   var src = fs.readFileSync(__dirname + "/fixtures/multiline-global-declarations.js", "utf8");
 
-  TestRun(test).test(src);
+  TestRun(test)
+    .addError(12, "'pi' is defined but never used.")
+    .test(src, { unused: true });
 
   test.done();
 };
@@ -579,13 +607,16 @@ exports.testCatchBlocks = function (test) {
   var src = fs.readFileSync(__dirname + '/fixtures/gh247.js', 'utf8');
 
   TestRun(test)
-    .addError(11, "'w' is not defined.")
+    .addError(19, "'w' is already defined.")
+    .addError(35, "'u2' used out of scope.")
+    .addError(36, "'w2' used out of scope.")
     .test(src, { es3: true, undef: true, devel: true });
 
   src = fs.readFileSync(__dirname + '/fixtures/gh618.js', 'utf8');
 
   TestRun(test)
     .addError(5, "Value of 'x' may be overwritten in IE 8 and earlier.")
+    .addError(15, "Value of 'y' may be overwritten in IE 8 and earlier.")
     .test(src, { es3: true, undef: true, devel: true });
 
   TestRun(test)
@@ -750,6 +781,9 @@ exports.testES6ModulesNamedExportsAffectUnused = function (test) {
     "};",
     "var x = 23;",
     "var z = 42;",
+    "let c = 2;",
+    "const d = 7;",
+    "export { c, d };",
     "export { a, x };",
     "export var b = { baz: 'baz' };",
     "export function boo() { return z; }",
@@ -763,8 +797,8 @@ exports.testES6ModulesNamedExportsAffectUnused = function (test) {
   ];
 
   TestRun(test)
-    .addError(16, "const 'c1u' is initialized to 'undefined'.")
-    .addError(16, "const 'c2u' is initialized to 'undefined'.")
+    .addError(19, "const 'c1u' is initialized to 'undefined'.")
+    .addError(19, "const 'c2u' is initialized to 'undefined'.")
     .test(src1, {
       esnext: true,
       unused: true
@@ -793,8 +827,9 @@ exports.testConstRedeclaration = function (test) {
   ];
 
   TestRun(test)
-      .addError(2, "const 'a' has already been declared.")
-      .addError(9, "const 'a' has already been declared.")
+      .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
       });
@@ -860,6 +895,11 @@ exports.testConstModification = function (test) {
     "delete a[0];",
     "new a();",
     "new a;",
+    "function e() {",
+    "  f++;",
+    "}",
+    "const f = 1;",
+    "e();"
   ];
 
   TestRun(test)
@@ -871,6 +911,7 @@ exports.testConstModification = function (test) {
       .addError(8, "Attempting to override 'a' which is a constant.")
       .addError(8, "You might be leaking a variable (a) here.")
       .addError(53, "Missing '()' invoking a constructor.")
+      .addError(55, "Attempting to override 'f' which is a constant.")
       .test(src, {
         esnext: true
       });
@@ -1430,3 +1471,136 @@ exports.beginningArraysAreNotJSON = function (test) {
   test.done();
 
 };
+
+exports.labelsOutOfScope = function (test) {
+  var src = [
+    "function a() {",
+    "  if (true) {",
+    "    bar: switch(2) {",
+    "    }",
+    "    foo: switch(1) {",
+    "      case 1:",
+    "        (function () {",
+    "          baz: switch(3) {",
+    "            case 3:",
+    "              break foo;",
+    "            case 2:",
+    "              break bar;",
+    "            case 3:",
+    "              break doesnotexist;",
+    "          }",
+    "        })();",
+    "        if (true) {",
+    "          break foo;",
+    "        }",
+    "        break foo;",
+    "      case 2:",
+    "        break bar;",
+    "      case 3:",
+    "        break baz;",
+    "    }",
+    "  }",
+    "}"
+  ];
+
+  TestRun(test)
+    .addError(10, "'foo' is not a statement label.")
+    .addError(12, "'bar' is not a statement label.")
+    .addError(14, "'doesnotexist' is not a statement label.")
+    .addError(22, "'bar' is not a statement label.")
+    .addError(24, "'baz' is not a statement label.")
+    .test(src);
+
+  test.done();
+};
+
+exports.labelThroughCatch = function (test) {
+  var src = [
+    "function labelExample() {",
+    "  'use strict';",
+    "  var i;",
+    "  example:",
+    "    for (i = 0; i < 10; i += 1) {",
+    "      try {",
+    "        if (i === 5) {",
+    "          break example;",
+    "        } else {",
+    "          throw new Error();",
+    "        }",
+    "      } catch (e) {",
+    "        continue example;",
+    "      }",
+    "    }",
+    "}"
+  ];
+
+  TestRun(test)
+    .test(src);
+
+  test.done();
+};
+
+exports.labelDoesNotExistInGlobalScope = function (test) {
+  var src = [
+    "switch(1) {",
+    "  case 1:",
+    "    break nonExistent;",
+    "}"
+  ];
+
+  TestRun(test)
+    .addError(3, "'nonExistent' is not a statement label.")
+    .test(src);
+
+  test.done();
+};
+
+exports.labelsContinue = function (test) {
+  var src = [
+    "exists: while(true) {",
+    "  if (false) {",
+    "    continue exists;",
+    "  }",
+    "  continue nonExistent;",
+    "}"
+  ];
+
+  TestRun(test)
+    .addError(5, "'nonExistent' is not a statement label.")
+    .test(src);
+
+  test.done();
+};
+
+exports.catchWithNoParam = function (test) {
+  var src = [
+    "try{}catch(){}"
+  ];
+
+  TestRun(test)
+    .addError(1, "Expected an identifier and instead saw ')'.")
+    .test(src);
+
+  test.done();
+};
+
+exports.catchWithNoParam = function (test) {
+  var src = [
+    "try{}",
+    "if (true) { console.log(); }"
+  ];
+
+  TestRun(test)
+    .addError(2, "Expected 'catch' and instead saw 'if'.")
+    .test(src);
+
+  var src = [
+    "try{}"
+  ];
+
+  TestRun(test)
+    .addError(1, "Expected 'catch' and instead saw ''.")
+    .test(src);
+
+  test.done();
+};
diff --git a/tests/unit/envs.js b/tests/unit/envs.js
index 0d0bd56d..c078ea3e 100644
--- a/tests/unit/envs.js
+++ b/tests/unit/envs.js
@@ -30,7 +30,7 @@ function globalsImplied(test, globals, options) {
   JSHINT(wrap(globals), options || {});
   var report = JSHINT.data();
 
-  test.ok(report.implieds !== null);
+  test.ok(report.implieds != null);
   test.ok(report.globals === undefined);
 
   var implieds = [];
diff --git a/tests/unit/fixtures/es6-import-export.js b/tests/unit/fixtures/es6-import-export.js
index a7bc81db..47f35227 100644
--- a/tests/unit/fixtures/es6-import-export.js
+++ b/tests/unit/fixtures/es6-import-export.js
@@ -6,9 +6,9 @@ import { one } from "foo";
 import { default as _ } from "underscore";
 import {} from "ember";
 import "ember";
-import * as ember from "ember";
-import _, * as ember from "ember";
-import _, { default as ember } from "ember";
+import * as ember2 from "ember";
+import _2, * as ember3 from "ember";
+import _3, { default as ember } from "ember";
 
 $.ajax();
 emGet("foo");
diff --git a/tests/unit/fixtures/gh247.js b/tests/unit/fixtures/gh247.js
index 5a55df63..663f5109 100644
--- a/tests/unit/fixtures/gh247.js
+++ b/tests/unit/fixtures/gh247.js
@@ -21,3 +21,17 @@ function test() {
 
     alert("w:" + w);
 }
+
+function infunc() {
+    var i2 = 5;
+
+    try {
+        var u2 = "I'm trying here!";
+    } catch (e) {
+        var w2 = "Let's play catch.";
+    }
+
+    alert("i2:" + i2);
+    alert("u2:" + u2);
+    alert("w2:" + w2);
+}
diff --git a/tests/unit/fixtures/gh618.js b/tests/unit/fixtures/gh618.js
index 07e8a033..f637d169 100644
--- a/tests/unit/fixtures/gh618.js
+++ b/tests/unit/fixtures/gh618.js
@@ -5,3 +5,11 @@ try {
 } catch (x) {}
 
 console.log(x);
+
+if (true) {
+    var y;
+}
+
+try {
+    throw "boom";
+} catch (y) {}
diff --git a/tests/unit/fixtures/latedef-esnext.js b/tests/unit/fixtures/latedef-esnext.js
new file mode 100644
index 00000000..908d746d
--- /dev/null
+++ b/tests/unit/fixtures/latedef-esnext.js
@@ -0,0 +1,55 @@
+let a = 1;
+let b = () => a;
+let d = () => c;
+let c = 1;
+let f = () => e;
+const e = 1;
+let g = () => { return h; };
+let h = 1;
+let j = i;
+let i = 1;
+function ag() {
+    function ah() {
+        ai = 2;
+    }
+    let ai;
+    function aj() {
+        function ak() {
+            ai = 3;
+        }
+        let ai;
+        ak();
+    }
+    ah();
+    aj();
+}
+ag();
+function bg() {
+    function bh() {
+        bi = 2;
+    }
+    let bi;
+    function bj() {
+        function bk() {
+            bi = 3;
+        }
+        bk();
+    }
+    bh();
+    bj();
+}
+bg();
+function cg() {
+    let ci;
+    function cj() {
+        function ck() {
+            ci = 3;
+        }
+        let ci;
+        ck();
+    }
+    ci = 2;
+    ch();
+    cj();
+}
+cg();
diff --git a/tests/unit/fixtures/loopfunc.js b/tests/unit/fixtures/loopfunc.js
index f88f67f5..7fd1ec36 100644
--- a/tests/unit/fixtures/loopfunc.js
+++ b/tests/unit/fixtures/loopfunc.js
@@ -25,3 +25,7 @@ function boo(p) {
     var c = function () { return p; };
   }
 }
+
+while (true) {
+  var d = function z() { return z(); };
+}
diff --git a/tests/unit/fixtures/newcap.js b/tests/unit/fixtures/newcap.js
index d99668f2..c5cf4d10 100644
--- a/tests/unit/fixtures/newcap.js
+++ b/tests/unit/fixtures/newcap.js
@@ -9,6 +9,11 @@ cat = Animal();
 var rat = new iAnimal();
 var bat = new myAnimal();
 
+(function() {
+  var iAnimal = function() {};
+  rat = new iAnimal();
+}());
+
 rat = iAnimal();
 bat = myAnimal();
 
diff --git a/tests/unit/fixtures/redef-es6.js b/tests/unit/fixtures/redef-es6.js
new file mode 100644
index 00000000..221ed8d1
--- /dev/null
+++ b/tests/unit/fixtures/redef-es6.js
@@ -0,0 +1,361 @@
+var ga;
+let ga;
+
+var gb;
+const gb = 1;
+
+var gc;
+if (gc) {
+    let gc;
+}
+
+let gd;
+if (gd) {
+    var gd;
+}
+
+var ge;
+if (ge) {
+    const ge = 1;
+}
+
+const gf = 1;
+if (gf) {
+    var gf;
+}
+
+gg: while(true) {
+    let gg;
+}
+
+gh: while(true) {
+    const gh = 1;
+}
+
+gi: while(true) {
+    var gi;
+}
+
+let gj;
+gj: while(true) {
+}
+
+const gk = 1;
+gk: while(true) {
+}
+
+var gl;
+gl: while(true) {
+}
+
+let gm;
+if (gm) {
+    gm: while (true) {
+    }
+}
+
+const gn = 1;
+if (gn) {
+    gn: while (true) {
+    }
+}
+
+var go;
+if (go) {
+    go: while (true) {
+    }
+}
+
+let gp;
+if (gp) {
+    let gp;
+}
+
+const gq = 1;
+if (gq) {
+    let gq;
+}
+
+let gr;
+if (gr) {
+    const gr = 1;
+}
+
+const gs = 1;
+if (gs) {
+    const gs = 1;
+}
+
+var gt;
+(function() {
+    let gt;
+}());
+
+let gu;
+(function() {
+    var gu;
+}());
+
+var gv;
+(function() {
+    const gv = 1;
+}());
+
+const gw = 1;
+(function() {
+    var gw;
+}());
+
+let gx;
+let gx;
+
+let gy;
+const gy = 1;
+
+const gz = 1;
+const gz = 1;
+
+let gza;
+var gza;
+
+const gzb = 1;
+var gzb;
+
+if (true) {
+    let gzc;
+}
+let gzc;
+
+if (true) {
+    var gzd;
+}
+let gzd;
+
+if (true) {
+    let gzd2;
+}
+var gzd2;
+
+if (true) {
+    const gze = 1;
+}
+const gze = 1;
+
+if (true) {
+    var gzf;
+}
+const gzf = 1;
+
+if (true) {
+    const gzg = 1;
+}
+var gzg;
+
+function zz() {
+    var a;
+    let a;
+
+    var b;
+    const b = 1;
+
+    var c;
+    if (c) {
+        let c;
+    }
+
+    let d;
+    if (d) {
+        var d;
+    }
+
+    var e;
+    if (e) {
+        const e = 1;
+    }
+
+    const f = 1;
+    if (f) {
+        var f;
+    }
+
+    g: while(true) {
+        let g;
+    }
+
+    h: while(true) {
+        const h = 1;
+    }
+
+    i: while(true) {
+        var i;
+    }
+
+    let j;
+    j: while(true) {
+    }
+
+    const k = 1;
+    k: while(true) {
+    }
+
+    var l;
+    l: while(true) {
+    }
+
+    let m;
+    if (m) {
+        m: while (true) {
+        }
+    }
+
+    const n = 1;
+    if (n) {
+        n: while (true) {
+        }
+    }
+
+    var o;
+    if (o) {
+        o: while (true) {
+        }
+    }
+
+    let p;
+    if (p) {
+        let p;
+    }
+
+    const q = 1;
+    if (q) {
+        let q;
+    }
+
+    let r;
+    if (r) {
+        const r = 1;
+    }
+
+    const s = 1;
+    if (s) {
+        const s = 1;
+    }
+
+    var t;
+    (function() {
+        let t;
+    }());
+
+    let u;
+    (function() {
+        var u;
+    }());
+
+    var v;
+    (function() {
+        const v = 1;
+    }());
+
+    const w = 1;
+    (function() {
+        var w;
+    }());
+
+    let x;
+    let x;
+
+    let y;
+    const y = 1;
+
+    const z = 1;
+    const z = 1;
+
+    let za;
+    var za;
+
+    const zb = 1;
+    var zb;
+
+    if (true) {
+        let zc;
+    }
+    let zc;
+
+    if (true) {
+        var zd;
+    }
+    let zd;
+
+    if (true) {
+        let zd2;
+    }
+    var zd2;
+
+    if (true) {
+        const ze = 1;
+    }
+    const ze = 1;
+
+    if (true) {
+        var zf;
+    }
+    const zf = 1;
+
+    if (true) {
+        const zg = 1;
+    }
+    var zg;
+
+    zh: while(true) {
+        break zh;
+    }
+    // works fine - zh is in scope of the while only
+    zh: while(true) {
+        break zh;
+    }
+
+    zi: while(true) {
+        zi: while(true) {
+            break zi;
+        }
+    }
+}
+
+function zz2() {
+    let zza = 1;
+    const zzb = 1;
+    let zzc = 1;
+    const zzd = 1;
+    let zze = 1;
+    const zzf = 1;
+    function y() {
+        function x() {
+            var zza = 2;
+            var zzb = 2;
+            let zzc = 2;
+            let zzd = 2;
+            let zze = 2;
+            let zzf = 2;
+        }
+    }
+}
+
+function zzg(zzh, zzi, zzj) {
+    var zzh = 1;
+    let zzi = 1;
+    const zzj = 1;
+}
+
+(function(zzk) { var zzk = zzk; return zzk; }());
+(function(zzl) { const zzl = zzl; return zzl; }());
+(function(zzm) { let zzm = zzm; return zzm; }());
+
+{
+    var zzn = null; // expected warning
+    const zzo = null;
+    let zzp = null;
+}
+(function() {
+    var zzn = null;
+    const zzo = null;
+    let zzp = null;
+}());
\ No newline at end of file
diff --git a/tests/unit/fixtures/redef.js b/tests/unit/fixtures/redef.js
index 3fbeb344..81ef7b1c 100644
--- a/tests/unit/fixtures/redef.js
+++ b/tests/unit/fixtures/redef.js
@@ -8,4 +8,13 @@
 
 function test(foo) {
     var foo = foo || false;
+}
+
+function z() {
+    var b = 1;
+    function y() {
+        function x() {
+            var b = 2;
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/unit/fixtures/undef.js b/tests/unit/fixtures/undef.js
index 4a4cca5a..065f2021 100644
--- a/tests/unit/fixtures/undef.js
+++ b/tests/unit/fixtures/undef.js
@@ -32,3 +32,7 @@ if (typeof((undef))) {}
 if (typeof undef()) {}
 if (typeof +undef) {}
 if (typeof(undef, 0)) {}
+
+function a() {
+    return a;
+}
diff --git a/tests/unit/fixtures/unused.js b/tests/unit/fixtures/unused.js
index d83a71cb..ccc4530c 100644
--- a/tests/unit/fixtures/unused.js
+++ b/tests/unit/fixtures/unused.js
@@ -68,4 +68,57 @@ let x = y => y;
 x = y => {};
 x = (y) => {};
 x = (y, z) => y;
-x = (y, z) => z;
\ No newline at end of file
+x = (y, z) => z;
+
+function throwAwayFuncA() { if (testLateDef) {} }
+let testLateDef = true;
+function throwAwayFuncB() { if (testLateDefConst) {} }
+const testLateDefConst = true;
+throwAwayFuncA();
+try {
+    var inTry = true;
+} catch(e){
+    var inCatch = true;
+}
+if (inTry) {
+    throwAwayFuncA();
+}
+if (inCatch) {
+    throwAwayFuncB();
+}
+(function() {
+    var inTry5 = 1;
+    var inTry6 = 1;
+    let inTry7 = 1;
+    const inTry8 = 1;
+    let inTry9 = 1;
+    const inTry10 = 1;
+    try {
+        var inTry2 = 1;
+        var inTry3 = 1;
+        var inTry4 = 1;
+    } catch(e){}
+    inTry2++;
+    (function() {
+        inTry3++;
+        inTry5++;
+        if (inTry8) {
+            inTry7++;
+        }
+    })();
+    (function() {
+        inTry4++;
+        var inTry4 = 1;
+        inTry6++;
+        var inTry6 = 1;
+        if (inTry10) {
+            inTry9++;
+        }
+        let inTry9 = 1;
+        const inTry10 = 1;
+    })();
+}());
+
+function unusedRecurringFunc() {
+    unusedRecurringFunc();
+}
diff --git a/tests/unit/options.js b/tests/unit/options.js
index d45022bb..5895b82a 100644
--- a/tests/unit/options.js
+++ b/tests/unit/options.js
@@ -48,7 +48,7 @@ exports.shadow = function (test) {
 };
 
 /**
- * Option `scopeshadow` allows you to re-define variables later in inner scopes.
+ * Option `shadow:outer` allows you to re-define variables later in inner scopes.
  *
  *  E.g.:
  *    var a = 1;
@@ -56,7 +56,7 @@ exports.shadow = function (test) {
  *        var a = 2;
  *    }
  */
-exports.scopeshadow = function (test) {
+exports.shadowouter = function (test) {
   var src = fs.readFileSync(__dirname + "/fixtures/scope-redef.js", "utf8");
 
   // Do not tolarate inner scope variable shadowing by default
@@ -86,6 +86,113 @@ exports.shadowInline = function (test) {
   test.done();
 };
 
+exports.shadowEs6 = function (test) {
+  var src = fs.readFileSync(__dirname + "/fixtures/redef-es6.js", "utf8");
+
+  var commonErrors = [
+    [2, "'ga' has already been declared."],
+    [5, "'gb' has already been declared."],
+    [14, "'gd' has already been declared."],
+    [24, "'gf' has already been declared."],
+    [110, "'gx' has already been declared."],
+    [113, "'gy' has already been declared."],
+    [116, "'gz' has already been declared."],
+    [119, "'gza' has already been declared."],
+    [122, "'gzb' has already been declared."],
+    [132, "'gzd' has already been declared."],
+    [147, "'gzf' has already been declared."],
+    [156, "'a' has already been declared."],
+    [159, "'b' has already been declared."],
+    [168, "'d' has already been declared."],
+    [178, "'f' has already been declared."],
+    [264, "'x' has already been declared."],
+    [267, "'y' has already been declared."],
+    [270, "'z' has already been declared."],
+    [273, "'za' has already been declared."],
+    [276, "'zb' has already been declared."],
+    [286, "'zd' has already been declared."],
+    [301, "'zf' has already been declared."],
+    [344, "'zzi' has already been declared."],
+    [345, "'zzj' has already been declared."],
+    [349, "'zzl' was used before it was declared, which is illegal for 'const' variables."],
+    [349, "'zzl' has already been declared."],
+    [350, "'zzm' was used before it was declared, which is illegal for 'let' variables."],
+    [350, "'zzm' has already been declared."]
+  ];
+
+  var innerErrors = [
+    [343, "'zzh' is already defined."],
+    [348, "'zzk' is already defined."]
+  ];
+
+  var outerErrors = [
+    /* block scope variables shadowing out of scope */
+    [9, "'gc' is already defined."],
+    [19, "'ge' is already defined."],
+    [28, "'gg' is already defined in outer scope."],
+    [32, "'gh' is already defined in outer scope."],
+    [36, "'gi' is already defined in outer scope."],
+    [40, "'gj' is already defined."],
+    [44, "'gk' is already defined."],
+    [48, "'gl' is already defined."],
+    [53, "'gm' is already defined."],
+    [59, "'gn' is already defined."],
+    [65, "'go' is already defined."],
+    [71, "'gp' is already defined."],
+    [76, "'gq' is already defined."],
+    [81, "'gr' is already defined."],
+    [86, "'gs' is already defined."],
+    [163, "'c' is already defined."],
+    [173, "'e' is already defined."],
+    [182, "'g' is already defined in outer scope."],
+    [186, "'h' is already defined in outer scope."],
+    [190, "'i' is already defined in outer scope."],
+    [194, "'j' is already defined."],
+    [198, "'k' is already defined."],
+    [202, "'l' is already defined."],
+    [207, "'m' is already defined."],
+    [213, "'n' is already defined."],
+    [219, "'o' is already defined."],
+    [225, "'p' is already defined."],
+    [230, "'q' is already defined."],
+    [235, "'r' is already defined."],
+    [240, "'s' is already defined."],
+    /* variables shadowing outside of function scope */
+    [91, "'gt' is already defined in outer scope."],
+    [96, "'gu' is already defined in outer scope."],
+    [101, "'gv' is already defined in outer scope."],
+    [106, "'gw' is already defined in outer scope."],
+    [245, "'t' is already defined in outer scope."],
+    [250, "'u' is already defined in outer scope."],
+    [255, "'v' is already defined in outer scope."],
+    [260, "'w' is already defined in outer scope."],
+    /* variables shadowing outside multiple function scopes */
+    [332, "'zza' is already defined in outer scope."],
+    [333, "'zzb' is already defined in outer scope."],
+    [334, "'zzc' is already defined in outer scope."],
+    [335, "'zzd' is already defined in outer scope."],
+    [336, "'zze' is already defined in outer scope."],
+    [337, "'zzf' is already defined in outer scope."],
+    [358, "'zzn' is already defined in outer scope."],
+    /* labels */
+    [317, "'zi' is already defined in outer scope."] // should always error
+  ];
+
+  var testRun = TestRun(test);
+  commonErrors.forEach(function(error) { testRun.addError.apply(testRun, error); });
+  testRun.test(src, {esnext: true, shadow: true});
+
+  var testRun = TestRun(test);
+  commonErrors.concat(innerErrors).forEach(function(error) { testRun.addError.apply(testRun, error); });
+  testRun.test(src, {esnext: true, shadow: "inner", maxerr: 100 });
+
+  var testRun = TestRun(test);
+  commonErrors.concat(innerErrors, outerErrors).forEach(function(error) { testRun.addError.apply(testRun, error); });
+  testRun.test(src, {esnext: true, shadow: "outer", maxerr: 100});
+
+  test.done();
+};
+
 /**
  * Option `latedef` allows you to prohibit the use of variable before their
  * definitions.
@@ -100,11 +207,16 @@ exports.shadowInline = function (test) {
  */
 exports.latedef = function (test) {
   var src  = fs.readFileSync(__dirname + '/fixtures/latedef.js', 'utf8'),
-    src1 = fs.readFileSync(__dirname + '/fixtures/redef.js', 'utf8');
+    src1 = fs.readFileSync(__dirname + '/fixtures/redef.js', 'utf8'),
+    esnextSrc = fs.readFileSync(__dirname + '/fixtures/latedef-esnext.js', 'utf8');
 
   // By default, tolerate the use of variable before its definition
   TestRun(test)
-    .test(src, {es3: true});
+    .test(src, {es3: true, funcscope: true});
+
+  TestRun(test)
+      .addError(10, "'i' was used before it was declared, which is illegal for 'let' variables.")
+      .test(esnextSrc, {esnext: true});
 
   // However, JSHint must complain if variable is actually missing
   TestRun(test)
@@ -127,9 +239,21 @@ exports.latedef = function (test) {
     .addError(2, "'fn' was used before it was defined.")
     .addError(6, "'fn1' was used before it was defined.")
     .addError(10, "'vr' was used before it was defined.")
+    .addError(18, "'bar' was used before it was defined.")
     .addError(18, "Inner functions should be listed at the top of the outer function.")
     .test(src, { es3: true, latedef: true });
 
+  TestRun(test)
+      .addError(4, "'c' was used before it was defined.")
+      .addError(6, "'e' was used before it was defined.")
+      .addError(8, "'h' was used before it was defined.")
+      .addError(10, "'i' was used before it was declared, which is illegal for 'let' variables.")
+      .addError(15, "'ai' was used before it was defined.")
+      .addError(20, "'ai' was used before it was defined.")
+      .addError(31, "'bi' was used before it was defined.")
+      .addError(48, "'ci' was used before it was defined.")
+      .test(esnextSrc, {esnext: true, latedef: true});
+
   test.done();
 };
 
@@ -580,6 +704,15 @@ exports.undef = function (test) {
     .addError(34, "'undef' is not defined.")
     .test(src, { es3: true, undef: true });
 
+  // block scope cannot use themselves in the declaration
+  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.")
+    .test([
+      'let a = a;',
+      'const b = b;'
+    ], { esnext: true, undef: true });
+
   // Regression test for GH-668.
   src = fs.readFileSync(__dirname + "/fixtures/gh668.js", "utf8");
   test.ok(JSHINT(src, { undef: true }));
@@ -589,7 +722,8 @@ exports.undef = function (test) {
   test.ok(!JSHINT.data().implieds);
 
   JSHINT("if (typeof foobar) {}", { undef: true });
-  test.ok(!JSHINT.data().implieds);
+  console.log(JSHINT.data().implieds);
+  test.strictEqual(JSHINT.data().implieds, undefined);
 
   test.done();
 };
@@ -632,24 +766,39 @@ exports.undefDeleteStrict = function (test) {
 exports.unused = function (test) {
   var src = fs.readFileSync(__dirname + '/fixtures/unused.js', 'utf8');
 
-  TestRun(test).test(src, { esnext: true });
+  var allErrors = [
+    [22, "'i' is defined but never used."],
+    [101, "'inTry2' used out of scope."],
+    [117, "'inTry9' was used before it was declared, which is illegal for 'let' variables."],
+    [118, "'inTry10' was used before it was declared, which is illegal for 'const' variables."]
+  ];
 
-  var var_errors = [
+  var testRun = TestRun(test);
+  allErrors.forEach(function (e) {
+    testRun.addError.apply(testRun, e);
+  });
+  testRun.test(src, { esnext: true });
+
+  var var_errors = allErrors.concat([
     [1, "'a' is defined but never used."],
     [7, "'c' is defined but never used."],
     [15, "'foo' is defined but never used."],
     [20, "'bar' is defined but never used."],
-    [22, "'i' is defined but never used."],
     [36, "'cc' is defined but never used."],
     [39, "'dd' is defined but never used."],
     [58, "'constUsed' is defined but never used."],
     [62, "'letUsed' is defined but never used."],
-    [63, "'anotherUnused' is defined but never used."]
-  ];
+    [63, "'anotherUnused' is defined but never used."],
+    [63, "'anotherUnused' is defined but never used."],
+    [91, "'inTry6' is defined but never used."],
+    [94, "'inTry9' is defined but never used."],
+    [95, "'inTry10' is defined but never used."],
+    [99, "'inTry4' is defined but never used."],
+    [122, "'unusedRecurringFunc' is defined but never used."]
+  ]);
 
   var last_param_errors = [
     [6, "'f' is defined but never used."],
-    [22, "'i' is defined but never used."],
     [28, "'a' is defined but never used."],
     [28, "'b' is defined but never used."],
     [28, "'c' is defined but never used."],
@@ -660,7 +809,6 @@ exports.unused = function (test) {
 
   var all_param_errors = [
     [15, "'err' is defined but never used."],
-    [22, "'i' is defined but never used."],
     [28, "'a' is defined but never used."],
     [28, "'b' is defined but never used."],
     [28, "'c' is defined but never used."],
@@ -690,7 +838,7 @@ exports.unused = function (test) {
   vars_run.test(src, { esnext: true, unused: "vars"});
 
   var unused = JSHINT.data().unused;
-  test.equal(19, unused.length);
+  test.equal(24, unused.length);
   test.ok(unused.some(function (err) { return err.line === 1 && err.character == 5 && err.name === "a"; }));
   test.ok(unused.some(function (err) { return err.line === 6 && err.character == 18 && err.name === "f"; }));
   test.ok(unused.some(function (err) { return err.line === 7 && err.character == 9 && err.name === "c"; }));
@@ -700,6 +848,76 @@ exports.unused = function (test) {
   test.done();
 };
 
+exports['unused data with options'] = function (test) {
+
+  // see gh-1894 for discussion on this test
+
+  var code = [
+    "function func(placeHolder1, placeHolder2, used, param) {",
+    "  used = 1;",
+    "}"
+  ];
+
+  var expectedVarUnused = [{ name: 'func', line: 1, character: 10 }];
+  var expectedParamUnused = [{ name: 'param', line: 1, character: 49 }];
+  var expectedPlaceholderUnused = [{ name: 'placeHolder2', line: 1, character: 29 },
+    { name: 'placeHolder1', line: 1, character: 15 }];
+
+  var expectedAllUnused = expectedParamUnused.concat(expectedPlaceholderUnused, expectedVarUnused);
+  var expectedVarAndParamUnused = expectedParamUnused.concat(expectedVarUnused);
+
+  // true
+  TestRun(test)
+    .addError(1, "'func' is defined but never used.")
+    .addError(1, "'param' is defined but never used.")
+    .test(code, { unused: true });
+
+  var unused = JSHINT.data().unused;
+  test.deepEqual(expectedVarAndParamUnused, unused);
+
+  // false
+  TestRun(test)
+    .test(code, { unused: false });
+
+  unused = JSHINT.data().unused;
+  test.deepEqual(expectedVarUnused, unused);
+
+  // strict
+  TestRun(test)
+    .addError(1, "'func' is defined but never used.")
+    .addError(1, "'placeHolder1' is defined but never used.")
+    .addError(1, "'placeHolder2' is defined but never used.")
+    .addError(1, "'param' is defined but never used.")
+    .test(code, { unused: "strict" });
+
+  unused = JSHINT.data().unused;
+  test.deepEqual(expectedAllUnused, unused);
+
+  // vars
+  TestRun(test)
+    .addError(1, "'func' is defined but never used.")
+    .test(code, { unused: "vars" });
+
+  unused = JSHINT.data().unused;
+  test.deepEqual(expectedAllUnused, unused);
+
+  test.done();
+};
+
+exports['unused with global override'] = function (test) {
+  var code;
+
+  code = [
+    "alert();",
+    "function alert() {}"
+  ];
+
+  TestRun(test)
+    .test(code, { unused: true, undef: true, devel: true, latedef: false });
+
+  test.done();
+};
+
 // Regressions for "unused" getting overwritten via comment (GH-778)
 exports['unused overrides'] = function (test) {
   var code;
@@ -744,6 +962,73 @@ exports['unused overrides'] = function (test) {
   test.done();
 };
 
+exports['unused overrides esnext'] = function (test) {
+  var code;
+
+  code = ['function foo(a) {', '/*jshint unused:false */', '}', 'foo();'];
+  TestRun(test).test(code, {esnext: true, unused: true});
+
+  code = ['function foo(a, b, c) {', '/*jshint unused:vars */', 'let i = b;', '}', 'foo();'];
+  TestRun(test)
+    .addError(3, "'i' is defined but never used.")
+    .test(code, {esnext: true, unused: true});
+
+  code = ['function foo(a, b, c) {', '/*jshint unused:true */', 'let i = b;', '}', 'foo();'];
+  TestRun(test)
+    .addError(1, "'c' is defined but never used.")
+    .addError(3, "'i' is defined but never used.")
+    .test(code, {esnext: true, unused: "strict"});
+
+  code = ['function foo(a, b, c) {', '/*jshint unused:strict */', 'let i = b;', '}', 'foo();'];
+  TestRun(test)
+    .addError(1, "'a' is defined but never used.")
+    .addError(1, "'c' is defined but never used.")
+    .addError(3, "'i' is defined but never used.")
+    .test(code, {esnext: true, unused: true});
+
+  code = ['/*jshint unused:vars */', 'function foo(a, b) {', 'let i = 3;', '}', 'foo();'];
+  TestRun(test)
+    .addError(3, "'i' is defined but never used.")
+    .test(code, {esnext: true, unused: "strict"});
+
+  code = ['/*jshint unused:badoption */', 'function foo(a, b) {', 'let i = 3;', '}', 'foo();'];
+  TestRun(test)
+    .addError(1, "Bad option value.")
+    .addError(2, "'b' is defined but never used.")
+    .addError(2, "'a' is defined but never used.")
+    .addError(3, "'i' is defined but never used.")
+    .test(code, {esnext: true, unused: "strict"});
+
+  test.done();
+};
+
+exports['unused with latedef function'] = function (test) {
+  var code;
+
+  // Regression for gh-2363, gh-2282, gh-2191
+  code = ['exports.a = a;',
+    'function a() {}',
+    'exports.b = function() { b(); };',
+    'function b() {}',
+    '(function() {',
+    '  function c() { d(); }',
+    '  window.c = c;',
+    '  function d() {}',
+    '})();',
+    'var e;',
+    '(function() {',
+    '  e();',
+    '  function e(){}',
+    '})();',
+    ''];
+
+  TestRun(test)
+    .addError(10, "'e' is defined but never used.")
+    .test(code, {undef: false, unused: true, node: true});
+
+  test.done();
+};
+
 // Regression test for `undef` to make sure that ...
 exports['undef in a function scope'] = function (test) {
   var src = fixture('undef_func.js');
@@ -825,6 +1110,7 @@ exports.loopfunc = function (test) {
     .addError(8, "Don't make functions within a loop.")
     .addError(20, "Don't make functions within a loop.")
     .addError(25, "Don't make functions within a loop.")
+    .addError(30, "Don't make functions within a loop.")
     .addError(12, "Function declarations should not be placed in blocks. Use a function " +
             "expression or move the statement to the top of the outer function.")
     .test(src, {es3: true});
@@ -1125,6 +1411,7 @@ exports.newcap = function (test) {
     .addError(1, 'A constructor name should start with an uppercase letter.')
     .addError(5, "Missing 'new' prefix when invoking a constructor.")
     .addError(10, "A constructor name should start with an uppercase letter.")
+    .addError(14, "A constructor name should start with an uppercase letter.")
     .test(src, { es3: true, newcap: true });
 
   test.done();
@@ -1431,12 +1718,12 @@ exports.esnext = function (test) {
     .test(src, { moz: true });
 
   TestRun(test)
-    .addError(3, "const 'myConst' has already been declared.")
+    .addError(3, "'myConst' has already been declared.")
     .addError(4, "Attempting to override 'foo' which is a constant.")
     .test(code, { esnext: true });
 
   TestRun(test)
-    .addError(3, "const 'myConst' has already been declared.")
+    .addError(3, "'myConst' has already been declared.")
     .addError(4, "Attempting to override 'foo' which is a constant.")
     .test(code, { moz: true });
 
@@ -2284,6 +2571,88 @@ exports.futureHostile = function (test) {
     .addError(9, "Redefinition of 'WeakSet'.")
     .test(code, { esnext: true, futurehostile: false });
 
+  TestRun(test, "ESNext with option")
+    .addError(1, "Redefinition of 'JSON'.")
+    .addError(2, "Redefinition of 'Map'.")
+    .addError(3, "Redefinition of 'Promise'.")
+    .addError(4, "Redefinition of 'Proxy'.")
+    .addError(5, "Redefinition of 'Reflect'.")
+    .addError(6, "Redefinition of 'Set'.")
+    .addError(7, "Redefinition of 'Symbol'.")
+    .addError(8, "Redefinition of 'WeakMap'.")
+    .addError(9, "Redefinition of 'WeakSet'.")
+    .test(code, { esnext: true });
+
+  TestRun(test, "ESNext with opt-out")
+    .test(code, {
+      esnext: true,
+      futurehostile: false,
+      predef: [
+        "-JSON",
+        "-Map",
+        "-Promise",
+        "-Proxy",
+        "-Reflect",
+        "-Set",
+        "-Symbol",
+        "-WeakMap",
+        "-WeakSet"
+      ]
+    });
+
+  code = [
+    "let JSON = {};",
+    "let Map = function() {};",
+    "let Promise = function() {};",
+    "let Proxy = function() {};",
+    "let Reflect = function() {};",
+    "let Set = function() {};",
+    "let Symbol = function() {};",
+    "let WeakMap = function() {};",
+    "let WeakSet = function() {};"
+  ];
+
+  TestRun(test, "ESNext with option")
+    .addError(1, "Redefinition of 'JSON'.")
+    .addError(2, "Redefinition of 'Map'.")
+    .addError(3, "Redefinition of 'Promise'.")
+    .addError(4, "Redefinition of 'Proxy'.")
+    .addError(5, "Redefinition of 'Reflect'.")
+    .addError(6, "Redefinition of 'Set'.")
+    .addError(7, "Redefinition of 'Symbol'.")
+    .addError(8, "Redefinition of 'WeakMap'.")
+    .addError(9, "Redefinition of 'WeakSet'.")
+    .test(code, { esnext: true });
+
+  TestRun(test, "ESNext with opt-out")
+    .test(code, {
+      esnext: true,
+      futurehostile: false,
+      predef: [
+        "-JSON",
+        "-Map",
+        "-Promise",
+        "-Proxy",
+        "-Reflect",
+        "-Set",
+        "-Symbol",
+        "-WeakMap",
+        "-WeakSet"
+      ]
+    });
+
+  code = [
+    "const JSON = {};",
+    "const Map = function() {};",
+    "const Promise = function() {};",
+    "const Proxy = function() {};",
+    "const Reflect = function() {};",
+    "const Set = function() {};",
+    "const Symbol = function() {};",
+    "const WeakMap = function() {};",
+    "const WeakSet = function() {};"
+  ];
+
   TestRun(test, "ESNext with option")
     .addError(1, "Redefinition of 'JSON'.")
     .addError(2, "Redefinition of 'Map'.")
diff --git a/tests/unit/parser.js b/tests/unit/parser.js
index 2148056f..6936ea53 100644
--- a/tests/unit/parser.js
+++ b/tests/unit/parser.js
@@ -1142,7 +1142,6 @@ exports["destructuring var in function scope"] = function (test) {
     .addError(12, "Expected an identifier and instead saw ']'.")
     .addError(12, "Expected an assignment or function call and instead saw an expression.")
     .addError(4,  "'z' is not defined.")
-    .addError(12, "'a' is defined but never used.")
     .addError(12, "'b' is defined but never used.")
     .addError(12, "'c' is defined but never used.")
     .addError(5,  "'h' is defined but never used.")
@@ -1310,7 +1309,6 @@ exports["destructuring var errors"] = function (test) {
     .addError(11, "Expected an identifier and instead saw ']'.")
     .addError(11, "Expected an assignment or function call and instead saw an expression.")
     .addError(3,  "'z' is not defined.")
-    .addError(11, "'a' is defined but never used.")
     .addError(11, "'b' is defined but never used.")
     .addError(11, "'c' is defined but never used.")
     .addError(4,  "'h' is defined but never used.")
@@ -1371,6 +1369,7 @@ exports["destructuring const as esnext"] = function (test) {
     "const [ f, [ [ [ g ], h ], i ] ] = [ 1, [ [ [ 2 ], 3], 4 ] ];",
     "const { foo : bar } = { foo : 1 };",
     "const [ j, { foo : foobar } ] = [ 2, { foo : 1 } ];",
+    "[j] = [1];"
   ];
 
   TestRun(test)
@@ -1387,8 +1386,8 @@ exports["destructuring const as esnext"] = function (test) {
     .addError(6, "'h' is defined but never used.")
     .addError(6, "'i' is defined but never used.")
     .addError(7, "'bar' is defined but never used.")
-    .addError(8, "'j' is defined but never used.")
     .addError(8, "'foobar' is defined but never used.")
+    .addError(9, "Attempting to override 'j' which is a constant.")
     .addError(3, "'z' is not defined.")
     .test(code, {esnext: true, unused: true, undef: true});
 
@@ -1517,9 +1516,9 @@ exports["destructuring const errors"] = function (test) {
     .addError(5, "'n' is defined but never used.")
     .addError(5, "'o' is defined but never used.")
     .addError(5, "'p' is defined but never used.")
-    .addError(2, "const 'a' has already been declared.")
-    .addError(2, "const 'b' has already been declared.")
-    .addError(2, "const 'c' has already been declared.")
+    .addError(2, "'a' has already been declared.")
+    .addError(2, "'b' has already been declared.")
+    .addError(2, "'c' has already been declared.")
     .addError(3, "Expected an identifier and instead saw '1'.")
     .addError(4, "Expected ',' and instead saw ';'.")
     .addError(5, "Expected ']' to match '[' from line 5 and instead saw ';'.")
@@ -2169,7 +2168,7 @@ exports["let statement in for loop as moz"] = function (test) {
     "for (let i = 0; i<15; ++i) {",
     "  print(i);",
     "}",
-    "for (let i=i ; i < 10 ; i++ ) {",
+    "for (let i=0 ; i < 10 ; i++ ) {",
     "print(i);",
     "}"
   ];
@@ -2195,7 +2194,7 @@ exports["let statement in for loop as esnext"] = function (test) {
     "for (let i = 0; i<15; ++i) {",
     "  print(i);",
     "}",
-    "for (let i=i ; i < 10 ; i++ ) {",
+    "for (let i=0 ; i < 10 ; i++ ) {",
     "print(i);",
     "}"
   ];
@@ -2221,7 +2220,7 @@ exports["let statement in for loop as es5"] = function (test) {
     "for (let i = 0; i<15; ++i) {",
     "  print(i);",
     "}",
-    "for (let i=i ; i < 10 ; i++ ) {",
+    "for (let i=0 ; i < 10 ; i++ ) {",
     "print(i);",
     "}"
   ];
@@ -2253,7 +2252,7 @@ exports["let statement in for loop as legacy JS"] = function (test) {
     "for (let i = 0; i<15; ++i) {",
     "  print(i);",
     "}",
-    "for (let i=i ; i < 10 ; i++ ) {",
+    "for (let i=0 ; i < 10 ; i++ ) {",
     "print(i);",
     "}"
   ];
@@ -2641,7 +2640,8 @@ exports["make sure var variables can shadow let variables"] = function (test) {
     .addError(1, "'a' is defined but never used.")
     .addError(2, "'b' is defined but never used.")
     .addError(3, "'c' is defined but never used.")
-    .addError(7, "'d' is defined but never used.")
+    .addError(9, "'d' is defined but never used.")
+    .addError(9, "'d' has already been declared.")
     .test(code, { esnext: true, unused: true, undef: true, funcscope: true });
 
   test.done();
@@ -2675,6 +2675,19 @@ exports["make sure let variables in the closure of blocks shadow predefined glob
   test.done();
 };
 
+exports["make sure variables may shadow globals in functions after they are referenced"] = function (test) {
+  var code = [
+    "var foo;",
+    "function x() {",
+    "  foo();",
+    "  var foo;",
+    "}"
+  ];
+
+  TestRun(test).test(code);
+  test.done();
+};
+
 exports["test destructuring function as moz"] = function (test) {
   // Example from https://developer.mozilla.org/en-US/docs/JavaScript/New_in_JavaScript/1.7
   var code = [
@@ -3097,6 +3110,32 @@ exports["array comprehension"] = function (test) {
   test.done();
 };
 
+exports["array comprehension unused and undefined"] = function (test) {
+  var code = [
+    "var range = [1, 2];",
+    "var a = [for (i of range) if (i % 2 === 0) i];",
+    "var b = [for (j of range) doesnotexist];"
+  ];
+  TestRun(test)
+    .addError(2, "'a' is defined but never used.")
+    .addError(3, "'j' is defined but never used.")
+    .addError(3, "'doesnotexist' is not defined.")
+    .addError(3, "'b' is defined but never used.")
+    .test(code, { esnext: true, unused: true, undef: true });
+
+  var unused = JSHINT.data().unused;
+  test.deepEqual([
+    { name: 'a', line: 2, character: 5 },
+    { name: 'b', line: 3, character: 5 }
+    //{ name: 'j', line: 3, character: 15 } // see gh-2440
+  ], unused);
+
+  var implieds = JSHINT.data().implieds;
+  test.deepEqual([{ name: 'doesnotexist', line: [ 3 ] }], implieds);
+
+  test.done();
+};
+
 exports["gh-1856 mistakenly identified as array comprehension"] = function (test) {
   var code = [
     "function main(value) {",
@@ -3700,12 +3739,13 @@ exports["for of as esnext"] = function (test) {
     "}",
     "for (let x of [1,2,3,4]) print(x);",
     "for (const x of [1,2,3,4]) print(x);",
-    "for (x = 1 of [1,2,3,4]) print(x);",
-    "for (x, y of [1,2,3,4]) print(x + y);",
-    "for (x = 1, y = 2 of [1,2,3,4]) print(x + y);",
-    "for (var x = 1 of [1,2,3,4]) print(x);",
-    "for (var x, y of [1,2,3,4]) print(x + y);",
-    "for (var x = 1, y = 2 of [1,2,3,4]) print(x + y);",
+    "var xg, yg;",
+    "for (xg = 1 of [1,2,3,4]) print(xg);",
+    "for (xg, yg of [1,2,3,4]) print(xg + yg);",
+    "for (xg = 1, yg = 2 of [1,2,3,4]) print(xg + yg);",
+    "for (var xv = 1 of [1,2,3,4]) print(xv);",
+    "for (var xv, yv of [1,2,3,4]) print(xv + yv);",
+    "for (var xv = 1, yv = 2 of [1,2,3,4]) print(xv + yv);",
     "for (let x = 1 of [1,2,3,4]) print(x);",
     "for (let x, y of [1,2,3,4]) print(x + y);",
     "for (let x = 1, y = 2 of [1,2,3,4]) print(x + y);",
@@ -3714,22 +3754,22 @@ exports["for of as esnext"] = function (test) {
     "for (const x = 1, y = 2 of [1,2,3,4]) print(x + y);"
   ];
   TestRun(test)
-    .addError(6, "Invalid for-of loop left-hand-side: initializer is forbidden.")
-    .addError(7, "Invalid for-of loop left-hand-side: more than one ForBinding.")
-    .addError(8, "Invalid for-of loop left-hand-side: initializer is forbidden.")
+    .addError(7, "Invalid for-of loop left-hand-side: initializer is forbidden.")
     .addError(8, "Invalid for-of loop left-hand-side: more than one ForBinding.")
     .addError(9, "Invalid for-of loop left-hand-side: initializer is forbidden.")
-    .addError(10, "Invalid for-of loop left-hand-side: more than one ForBinding.")
-    .addError(11, "Invalid for-of loop left-hand-side: initializer is forbidden.")
+    .addError(9, "Invalid for-of loop left-hand-side: more than one ForBinding.")
+    .addError(10, "Invalid for-of loop left-hand-side: initializer is forbidden.")
     .addError(11, "Invalid for-of loop left-hand-side: more than one ForBinding.")
     .addError(12, "Invalid for-of loop left-hand-side: initializer is forbidden.")
-    .addError(13, "Invalid for-of loop left-hand-side: more than one ForBinding.")
-    .addError(14, "Invalid for-of loop left-hand-side: initializer is forbidden.")
+    .addError(12, "Invalid for-of loop left-hand-side: more than one ForBinding.")
+    .addError(13, "Invalid for-of loop left-hand-side: initializer is forbidden.")
     .addError(14, "Invalid for-of loop left-hand-side: more than one ForBinding.")
     .addError(15, "Invalid for-of loop left-hand-side: initializer is forbidden.")
-    .addError(16, "Invalid for-of loop left-hand-side: more than one ForBinding.")
-    .addError(17, "Invalid for-of loop left-hand-side: initializer is forbidden.")
+    .addError(15, "Invalid for-of loop left-hand-side: more than one ForBinding.")
+    .addError(16, "Invalid for-of loop left-hand-side: initializer is forbidden.")
     .addError(17, "Invalid for-of loop left-hand-side: more than one ForBinding.")
+    .addError(18, "Invalid for-of loop left-hand-side: initializer is forbidden.")
+    .addError(18, "Invalid for-of loop left-hand-side: more than one ForBinding.")
     .test(code, {esnext: true, undef: true, predef: ["print"]});
 
   test.done();
@@ -5506,6 +5546,20 @@ exports.classExpression = function (test) {
   test.done();
 };
 
+exports.functionNotOverwritten = function (test) {
+  var code = [
+    "function x() {",
+    "  x = 1;",
+    "  var x;",
+    "}"
+  ];
+
+  TestRun(test)
+    .test(code, { shadow: true });
+
+  test.done();
+};
+
 exports.classExpressionThis = function (test) {
   var code = [
     "void class MyClass {",
@@ -6347,6 +6401,8 @@ exports.testES6UnusedExports = function (test) {
   ];
 
   TestRun(test)
+    .addError(24, "'letDefinedLater' was used before it was declared, which is illegal for 'let' variables.")
+    .addError(25, "'constDefinedLater' was used before it was declared, which is illegal for 'const' variables.")
     .test(code, { esnext: true, unused: true });
 
   test.done();