Repository: jshint/jshint
Pull request #2306 (2015-04-13 14:23:45)

[[FEAT]] Impement `module` option
@rwaldron @caitp I took a few liberties while implementing this. These aren't (*ahem*) "strictly" necessary, but they've always bugged me, and improving them now made the "module" option easier to implement. They seem pretty low-risk to me, but I've thought that before, so I'd appreciate your review of those changes in particular. (And if you don't think they're improvements, don't hesitate to say so.) They're contained in separate commits to help review (and so I can use commit messages to share justification).

I've introduced a new warning to help people avoid using the new option incorrectly. I've tried to make this generic enough for re-use with other ES6 options, and even for use in future versions.

Note that I've declared this under the "environments" collection of options. My first choice was "enforcing", but I changed my mind while working on the patch. Although it doesn't introduce new identifiers, it does describe an environmental change, so it kind of makes more sense to go here. It's not really "enforcing" *per se*, and people using `enforceall` would be especially surprised if the next release started treating all their code as strict mode code. (I can't wait to get rid of that option, by the way.)

Ideas for future extensions:

- Any minor release - Make this a "value" option and allow three different values: `true`, `false`, and `"infer"`. That last one would use a heuristic to automatically detect whether the code was module code. As Cait pointed out, this could never be foolproof, but by exposing the behavior through an explicitly-named option value, we can have *some* confidence that the user understands the edges before opting in. The benefit here would be that users wouldn't have to special-case their configuration according to their file structure.
- Any major release - Warn on `import` and `export` statements in any code that does not use `module: true` (or `module: "infer"` as the case may be).

If you think that first idea is worthwhile, I could implement it as part of this pull request. But the second one is backwards-breaking, so it will have to wait until 3.0.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2325574/badge)](https://coveralls.io/builds/2325574)

Coverage remained the same at 96.34% when pulling **a913cff17c47b3c5ae50d048c3b1c2f4a5294389 on jugglinmike:module-option** into **dd768c243af2bd3a8beb5e47768eef3ec7ab2e5e on jshint:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2325574/badge)](https://coveralls.io/builds/2325574)

Coverage remained the same at 96.34% when pulling **a913cff17c47b3c5ae50d048c3b1c2f4a5294389 on jugglinmike:module-option** into **dd768c243af2bd3a8beb5e47768eef3ec7ab2e5e on jshint:master**.

caitp: The patch looks good.

I'm really not keen on the "infer" idea, I think this could lead to confusion. Explicit is better, imo.

Can you make sure `// jshint module: true` works properly (eg at the head of the file)?
rwaldron: This is surprisingly simple, I like it. 

> I'm really not keen on the "infer" idea, I think this could lead to confusion. Explicit is better, imo.

I'm with @caitp on this. 


> Any major release - Warn on import and export statements in any code that does not use module: true (or module: "infer" as the case may be).

Very cool.


------

Things...

- @caitp asked for confirmation of `// jshint module: true` inline/in-file, which I just tried and it works, but please add a test for this. 

- When I add `"module": true`  to a .jshintrc, all files will be treated as a module, which is what I would expect. 
- What about allowing (in some future extension of the feature)...
  - `"module": [array of file name strings]`
  - `"module": "glob expression"`
  - `"module": [array of file name strings or glob expressions]`
- Maybe that ^^^ is an option called `"modules"` (with an "s")?






jugglinmike: > What about allowing (in some future extension of the feature)...
>
> - "module": [array of file name strings]
> - "module": "glob expression"
> - "module": [array of file name strings or glob expressions]
>
> Maybe that ^^^ is an option called "modules" (with an "s")?

It sounds like you have gh-1636 in mind.

The good news is that JSHint already exposes a general mechanism for this--`overrides`. (I only learned about this through the recent gh-2296, but there is [an open issue to document it](https://github.com/jshint/jshint.github.io/issues/17).) This means that we can land this as-is without including additional filesystem code :)
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2334310/badge)](https://coveralls.io/builds/2334310)

Coverage remained the same at 96.34% when pulling **8cad1199c573fc76cc7aa2acca5c07de1d58d761 on jugglinmike:module-option** into **dd768c243af2bd3a8beb5e47768eef3ec7ab2e5e on jshint:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2334310/badge)](https://coveralls.io/builds/2334310)

Coverage remained the same at 96.34% when pulling **8cad1199c573fc76cc7aa2acca5c07de1d58d761 on jugglinmike:module-option** into **dd768c243af2bd3a8beb5e47768eef3ec7ab2e5e on jshint:master**.

jugglinmike: Thanks for the review! As requested, I've added an additional test for in-line directive. This raises another question: should we forbid use of this directive in function scope? It doesn't make much sense to write:

```js
class Foo {
  bar() {
    /* jshint module: true */
  }
}
```

...but that's possible with this patch in its current state. Is it worth adding code to disallow weird uses like that? Is it likely that doing so would help anyone?
caitp: > This raises another question: should we forbid use of this directive in function scope? It doesn't make much sense to write:

I was saying this above --- I think the module directive is kind of special, in that you really need it to be one of the first items in the file (before any non-comment tokens)
jugglinmike: > I was saying this above

Sorry, I completely missed that. I'll see what I can do about this tonight. I think this would generally be fitting for *all* "environment" options. Obviously we can't do that in a minor release, but I'll try to build it so we can restrict `node: true` (for example) this way later.
jugglinmike: Note: we just received a feature request to turn `devel` off on a line-by-line basis (in the #jshint IRC channel), so this idea:

> I think this would generally be fitting for all "environment" options.

May not be appropriate after all.
caitp: I think you need to distinguish between certain methods being "safe" or " allowed", and an operating environment like "this is a module or script, browser or node, web worker or what" etc. Allowing reference to certain globals could certainly be enabled on a line by line basis, but nothing will change the intended environment of a file
jugglinmike: Okay, as discussed, the option is now disallowed outside of ES6 code and after executable code has been parsed. Sorry for the delay.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2490555/badge)](https://coveralls.io/builds/2490555)

Coverage increased (+0.0%) to 96.37% when pulling **84bf0becc76fd4d4f170876d4f7e156a22a23b02 on jugglinmike:module-option** into **b3b41c8555da07eaeacb0648cdc8cbbec62c2f59 on jshint:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2490555/badge)](https://coveralls.io/builds/2490555)

Coverage increased (+0.0%) to 96.37% when pulling **84bf0becc76fd4d4f170876d4f7e156a22a23b02 on jugglinmike:module-option** into **b3b41c8555da07eaeacb0648cdc8cbbec62c2f59 on jshint:master**.

jugglinmike: Rebased! I haven't received any feedback on the additional restrictions added by the two `fixup!`- commits. If those look good, I'll squash an merge.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2526536/badge)](https://coveralls.io/builds/2526536)

Coverage increased (+0.0%) to 96.47% when pulling **63f1f44f945bb9bc70df9092ad5ba2e91f8624d1 on jugglinmike:module-option** into **c0edd9ff84a68946c615198a4a6f188f6fbbd54d on jshint:master**.

rwaldron: LGTM, I'll let you make the call regarding my comment about refactoring the Boolean arg.
jugglinmike: Nice call, @rwaldron . In some cases, the parameter was completely unnecessary!
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2526660/badge)](https://coveralls.io/builds/2526660)

Coverage increased (+0.05%) to 96.52% when pulling **26557c4fc9085078914622319332bedab52ba696 on jugglinmike:module-option** into **c0edd9ff84a68946c615198a4a6f188f6fbbd54d on jshint:master**.

rwaldron: Oh, that's great news

Patch:
diff --git a/src/jshint.js b/src/jshint.js
index 07229909..ba9ed182 100644
--- a/src/jshint.js
+++ b/src/jshint.js
@@ -139,7 +139,7 @@ var JSHINT = (function() {
     }
     var meta = token.meta;
 
-    if (meta && meta.isFutureReservedWord && state.option.inES5()) {
+    if (meta && meta.isFutureReservedWord && state.inES5()) {
       // ES3 FutureReservedWord in an ES5 environment.
       if (!meta.es5) {
         return false;
@@ -148,7 +148,7 @@ var JSHINT = (function() {
       // Some ES5 FutureReservedWord identifiers are active only
       // within a strict mode environment.
       if (meta.strictOnly) {
-        if (!state.option.strict && !state.directive["use strict"]) {
+        if (!state.option.strict && !state.isStrict()) {
           return false;
         }
       }
@@ -202,6 +202,22 @@ var JSHINT = (function() {
       combine(predefined, vars.ecmaIdentifiers[6]);
     }
 
+    if (state.option.module) {
+      /**
+       * TODO: Extend this restriction to *all* "environmental" options.
+       */
+      if (!hasParsedCode(funct)) {
+        error("E055", state.tokens.next, "module");
+      }
+
+      /**
+       * TODO: Extend this restriction to *all* ES6-specific options.
+       */
+      if (!state.inESNext()) {
+        warning("W134", state.tokens.next, "module", 6);
+      }
+    }
+
     if (state.option.couch) {
       combine(predefined, vars.couch);
     }
@@ -289,33 +305,6 @@ var JSHINT = (function() {
     if (state.option.mocha) {
       combine(predefined, vars.mocha);
     }
-
-    // Let's assume that chronologically ES3 < ES5 < ES6/ESNext < Moz
-
-    state.option.inMoz = function(strict) {
-      if (strict) {
-        return state.option.moz && !state.option.esnext;
-      }
-      return state.option.moz;
-    };
-
-    state.option.inESNext = function(strict) {
-      if (strict) {
-        return !state.option.moz && state.option.esnext;
-      }
-      return state.option.moz || state.option.esnext;
-    };
-
-    state.option.inES5 = function(/* strict */) {
-      return !state.option.es3;
-    };
-
-    state.option.inES3 = function(strict) {
-      if (strict) {
-        return !state.option.moz && !state.option.esnext && state.option.es3;
-      }
-      return state.option.es3;
-    };
   }
 
   // Produce an error warning.
@@ -824,7 +813,7 @@ var JSHINT = (function() {
     if (next.id === ";" || next.id === "}" || next.id === ":") {
       return true;
     }
-    if (isInfix(next) === isInfix(curr) || (curr.id === "yield" && state.option.inMoz(true))) {
+    if (isInfix(next) === isInfix(curr) || (curr.id === "yield" && state.inMoz())) {
       return curr.line !== startLine(next);
     }
     return false;
@@ -855,7 +844,7 @@ var JSHINT = (function() {
 
     // if current expression is a let expression
     if (!initial && state.tokens.next.value === "let" && peek(0).value === "(") {
-      if (!state.option.inMoz(true)) {
+      if (!state.inMoz()) {
         warning("W118", state.tokens.next, "let expressions");
       }
       isLetExpr = true;
@@ -989,7 +978,7 @@ var JSHINT = (function() {
       nobreakcomma(state.tokens.prev, state.tokens.curr);
     }
 
-    if (state.tokens.next.identifier && !(opts.property && state.option.inES5())) {
+    if (state.tokens.next.identifier && !(opts.property && state.inES5())) {
       // Keywords that cannot follow a comma operator.
       switch (state.tokens.next.value) {
       case "break":
@@ -1246,16 +1235,16 @@ var JSHINT = (function() {
   // Checks whether the 'typeof' operator is used with the correct
   // value. For docs on 'typeof' see:
   // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
-  function isTypoTypeof(left, right, option) {
+  function isTypoTypeof(left, right, state) {
     var values;
 
-    if (option.notypeof)
+    if (state.option.notypeof)
       return false;
 
     if (!left || !right)
       return false;
 
-    values = option.inESNext() ? typeofValues.es6 : typeofValues.es3;
+    values = state.inESNext() ? typeofValues.es6 : typeofValues.es3;
 
     if (right.type === "(identifier)" && right.value === "typeof" && left.type === "(string)")
       return !_.contains(values, left.value);
@@ -1337,7 +1326,7 @@ var JSHINT = (function() {
         if (left.id === ".") {
           if (!left.left) {
             warning("E031", that);
-          } else if (left.left.value === "arguments" && !state.directive["use strict"]) {
+          } else if (left.left.value === "arguments" && !state.isStrict()) {
             warning("E031", that);
           }
 
@@ -1353,7 +1342,7 @@ var JSHINT = (function() {
             });
           } else if (!left.left) {
             warning("E031", that);
-          } else if (left.left.value === "arguments" && !state.directive["use strict"]) {
+          } else if (left.left.value === "arguments" && !state.isStrict()) {
             warning("E031", that);
           }
 
@@ -1467,7 +1456,7 @@ var JSHINT = (function() {
     }
 
     if (prop) {
-      if (state.option.inES5()) {
+      if (state.inES5()) {
         return val;
       }
     }
@@ -1592,7 +1581,7 @@ var JSHINT = (function() {
     // detect a module import declaration
     if (t.value === "module" && t.type === "(identifier)") {
       if (peek().type === "(identifier)") {
-        if (!state.option.inESNext()) {
+        if (!state.inESNext()) {
           warning("W119", state.tokens.curr, "module");
         }
 
@@ -1640,7 +1629,7 @@ var JSHINT = (function() {
     r = expression(0, true);
 
     if (r && (!r.identifier || r.value !== "function") && (r.type !== "(punctuator)")) {
-      if (!state.directive["use strict"] &&
+      if (!state.isStrict() &&
           state.option.globalstrict &&
           state.option.strict) {
         warning("E007");
@@ -1793,7 +1782,7 @@ var JSHINT = (function() {
           directives();
 
           if (state.option.strict && funct["(context)"]["(global)"]) {
-            if (!m["use strict"] && !state.directive["use strict"]) {
+            if (!m["use strict"] && !state.isStrict()) {
               warning("E007");
             }
           }
@@ -1818,7 +1807,7 @@ var JSHINT = (function() {
     } else if (!ordinary) {
       if (isfunc) {
         m = {};
-        if (stmt && !isfatarrow && !state.option.inMoz(true)) {
+        if (stmt && !isfatarrow && !state.inMoz()) {
           error("W118", state.tokens.curr, "function closure expressions");
         }
 
@@ -1832,7 +1821,7 @@ var JSHINT = (function() {
         expression(10);
 
         if (state.option.strict && funct["(context)"]["(global)"]) {
-          if (!m["use strict"] && !state.directive["use strict"]) {
+          if (!m["use strict"] && !state.isStrict()) {
             warning("E007");
           }
         }
@@ -2107,7 +2096,7 @@ var JSHINT = (function() {
   reserve("default").reach = true;
   reserve("finally");
   reservevar("arguments", function(x) {
-    if (state.directive["use strict"] && funct["(global)"]) {
+    if (state.isStrict() && funct["(global)"]) {
       warning("E008", x);
     }
   });
@@ -2116,7 +2105,7 @@ var JSHINT = (function() {
   reservevar("Infinity");
   reservevar("null");
   reservevar("this", function(x) {
-    if (state.directive["use strict"] && !isMethod() &&
+    if (state.isStrict() && !isMethod() &&
         !state.option.validthis && ((funct["(statement)"] &&
         funct["(name)"].charAt(0) > "Z") || funct["(global)"])) {
       warning("W040", x);
@@ -2197,10 +2186,10 @@ var JSHINT = (function() {
       case isPoorRelation(right):
         warning("W041", this, "===", right.value);
         break;
-      case isTypoTypeof(right, left, state.option):
+      case isTypoTypeof(right, left, state):
         warning("W122", this, right.value);
         break;
-      case isTypoTypeof(left, right, state.option):
+      case isTypoTypeof(left, right, state):
         warning("W122", this, left.value);
         break;
     }
@@ -2208,9 +2197,9 @@ var JSHINT = (function() {
     return this;
   });
   relation("===", function(left, right) {
-    if (isTypoTypeof(right, left, state.option)) {
+    if (isTypoTypeof(right, left, state)) {
       warning("W122", this, right.value);
-    } else if (isTypoTypeof(left, right, state.option)) {
+    } else if (isTypoTypeof(left, right, state)) {
       warning("W122", this, left.value);
     }
     return this;
@@ -2226,17 +2215,17 @@ var JSHINT = (function() {
       warning("W041", this, "!==", left.value);
     } else if (isPoorRelation(right)) {
       warning("W041", this, "!==", right.value);
-    } else if (isTypoTypeof(right, left, state.option)) {
+    } else if (isTypoTypeof(right, left, state)) {
       warning("W122", this, right.value);
-    } else if (isTypoTypeof(left, right, state.option)) {
+    } else if (isTypoTypeof(left, right, state)) {
       warning("W122", this, left.value);
     }
     return this;
   });
   relation("!==", function(left, right) {
-    if (isTypoTypeof(right, left, state.option)) {
+    if (isTypoTypeof(right, left, state)) {
       warning("W122", this, right.value);
-    } else if (isTypoTypeof(left, right, state.option)) {
+    } else if (isTypoTypeof(left, right, state)) {
       warning("W122", this, left.value);
     }
     return this;
@@ -2317,7 +2306,7 @@ var JSHINT = (function() {
 
     // The `delete` operator accepts unresolvable references when not in strict
     // mode, so the operand may be undefined.
-    if (p.identifier && !state.directive["use strict"]) {
+    if (p.identifier && !state.isStrict()) {
       p.forgiveUndef = true;
     }
     return this;
@@ -2474,7 +2463,7 @@ var JSHINT = (function() {
     if (left && left.value === "arguments" && (m === "callee" || m === "caller")) {
       if (state.option.noarg)
         warning("W059", left, m);
-      else if (state.directive["use strict"])
+      else if (state.isStrict())
         error("E008");
     } else if (!state.option.evil && left && left.value === "document" &&
         (m === "write" || m === "writeln")) {
@@ -2526,7 +2515,7 @@ var JSHINT = (function() {
     advance(")");
 
     if (typeof left === "object") {
-      if (state.option.inES3() && left.value === "parseInt" && n === 1) {
+      if (state.inES3() && left.value === "parseInt" && n === 1) {
         warning("W065", state.tokens.curr);
       }
       if (!state.option.evil) {
@@ -2706,7 +2695,7 @@ var JSHINT = (function() {
     var reversed = false;
     if (state.tokens.next.value !== "for") {
       reversed = true;
-      if (!state.option.inMoz(true)) {
+      if (!state.inMoz()) {
         warning("W116", state.tokens.next, "for", state.tokens.next.value);
       }
       funct["(comparray)"].setState("use");
@@ -2716,7 +2705,7 @@ var JSHINT = (function() {
     advance("for");
     if (state.tokens.next.value === "each") {
       advance("each");
-      if (!state.option.inMoz(true)) {
+      if (!state.inMoz()) {
         warning("W118", state.tokens.curr, "for each");
       }
     }
@@ -2753,11 +2742,11 @@ var JSHINT = (function() {
   prefix("[", function() {
     var blocktype = lookupBlockType();
     if (blocktype.isCompArray) {
-      if (!state.option.inESNext()) {
+      if (!state.inESNext()) {
         warning("W119", state.tokens.curr, "array comprehension");
       }
       return comprehensiveArrayExpression();
-    } else if (blocktype.isDestAssign && !state.option.inESNext()) {
+    } else if (blocktype.isDestAssign && !state.inESNext()) {
       warning("W104", state.tokens.curr, "destructuring assignment");
     }
     var b = state.tokens.curr.line !== startLine(state.tokens.next);
@@ -2771,7 +2760,7 @@ var JSHINT = (function() {
     while (state.tokens.next.id !== "(end)") {
       while (state.tokens.next.id === ",") {
         if (!state.option.elision) {
-          if (!state.option.inES5()) {
+          if (!state.inES5()) {
             // Maintain compat with old options --- ES5 mode without
             // elision=true will warn once per comma
             warning("W070");
@@ -2793,7 +2782,7 @@ var JSHINT = (function() {
       this.first.push(expression(10));
       if (state.tokens.next.id === ",") {
         comma({ allowTrailing: true });
-        if (state.tokens.next.id === "]" && !state.option.inES5(true)) {
+        if (state.tokens.next.id === "]" && !state.inES5(true)) {
           warning("W070", state.tokens.curr);
           break;
         }
@@ -2917,7 +2906,7 @@ var JSHINT = (function() {
         }
       }
       if (state.tokens.next.id === "=") {
-        if (!state.option.inESNext()) {
+        if (!state.inESNext()) {
           warning("W119", state.tokens.next, "default parameters");
         }
         advance("=");
@@ -2981,6 +2970,17 @@ var JSHINT = (function() {
     return "(scope)" in token;
   }
 
+  /**
+   * Determine if the parser has begun parsing executable code.
+   *
+   * @param {Token} funct - The current "functor" token
+   *
+   * @returns {boolean}
+   */
+  function hasParsedCode(funct) {
+    return funct["(global)"] && !funct["(verb)"];
+  }
+
   function doTemplateLiteral(left) {
     // ASSERT: this.type === "(template)"
     // jshint validthis: true
@@ -3188,7 +3188,7 @@ var JSHINT = (function() {
    */
   function checkProperties(props) {
     // Check for lonely setters if in the ES5 mode.
-    if (state.option.inES5()) {
+    if (state.inES5()) {
       for (var name in props) {
         if (_.has(props, name) && props[name].setterToken && !props[name].getterToken) {
           warning("W078", props[name].setterToken);
@@ -3218,7 +3218,7 @@ var JSHINT = (function() {
         nextVal = state.tokens.next.value;
         if (state.tokens.next.identifier &&
             (peekIgnoreEOL().id === "," || peekIgnoreEOL().id === "}")) {
-          if (!state.option.inESNext()) {
+          if (!state.inESNext()) {
             warning("W104", state.tokens.next, "object short notation");
           }
           i = propertyName(true);
@@ -3229,7 +3229,7 @@ var JSHINT = (function() {
         } else if (peek().id !== ":" && (nextVal === "get" || nextVal === "set")) {
           advance(nextVal);
 
-          if (!state.option.inES5()) {
+          if (!state.inES5()) {
             error("E034");
           }
 
@@ -3238,7 +3238,7 @@ var JSHINT = (function() {
           // ES6 allows for get() {...} and set() {...} method
           // definition shorthand syntax, so we don't produce an error
           // if the esnext option is enabled.
-          if (!i && !state.option.inESNext()) {
+          if (!i && !state.inESNext()) {
             error("E035");
           }
 
@@ -3260,7 +3260,7 @@ var JSHINT = (function() {
           }
         } else {
           if (state.tokens.next.value === "*" && state.tokens.next.type === "(punctuator)") {
-            if (!state.option.inESNext()) {
+            if (!state.inESNext()) {
               warning("W104", state.tokens.next, "generator functions");
             }
             advance("*");
@@ -3281,7 +3281,7 @@ var JSHINT = (function() {
           }
 
           if (state.tokens.next.value === "(") {
-            if (!state.option.inESNext()) {
+            if (!state.inESNext()) {
               warning("W104", state.tokens.curr, "concise methods");
             }
             doFunction({ type: g ? "generator" : null });
@@ -3297,7 +3297,7 @@ var JSHINT = (function() {
           comma({ allowTrailing: true, property: true });
           if (state.tokens.next.id === ",") {
             warning("W070", state.tokens.curr);
-          } else if (state.tokens.next.id === "}" && !state.option.inES5(true)) {
+          } else if (state.tokens.next.id === "}" && !state.inES5(true)) {
             warning("W070", state.tokens.curr);
           }
         } else {
@@ -3321,7 +3321,7 @@ var JSHINT = (function() {
   function destructuringExpression() {
     var id, ids;
     var identifiers = [];
-    if (!state.option.inESNext()) {
+    if (!state.inESNext()) {
       warning("W104", state.tokens.curr, "destructuring expression");
     }
     var nextInnerDE = function() {
@@ -3423,12 +3423,12 @@ var JSHINT = (function() {
     var isConst = type === "const";
     var tokens, lone, value, letblock;
 
-    if (!state.option.inESNext()) {
+    if (!state.inESNext()) {
       warning("W104", state.tokens.curr, type);
     }
 
     if (isLet && state.tokens.next.value === "(") {
-      if (!state.option.inMoz(true)) {
+      if (!state.inMoz()) {
         warning("W118", state.tokens.next, "let block");
       }
       advance("(");
@@ -3455,7 +3455,7 @@ var JSHINT = (function() {
       for (var t in tokens) {
         if (tokens.hasOwnProperty(t)) {
           t = tokens[t];
-          if (state.option.inESNext()) {
+          if (state.inESNext()) {
             // only look in the latest scope because we can shadow
             if (funct["(blockscope)"].current.labeltype(t.id) === "const") {
               warning("E011", null, t.id);
@@ -3550,7 +3550,7 @@ var JSHINT = (function() {
       for (var t in tokens) {
         if (tokens.hasOwnProperty(t)) {
           t = tokens[t];
-          if (state.option.inESNext()) {
+          if (state.inESNext()) {
             // because var is function scoped, look in the whole function
             if (funct["(blockscope)"].labeltype(t.id) === "const") {
               warning("E011", null, t.id);
@@ -3560,8 +3560,8 @@ var JSHINT = (function() {
             if (predefined[t.id] === false) {
               warning("W079", t.token, t.id);
             } else if (state.option.futurehostile === false) {
-              if ((!state.option.inES5() && vars.ecmaIdentifiers[5][t.id] === false) ||
-                  (!state.option.inESNext() && vars.ecmaIdentifiers[6][t.id] === false)) {
+              if ((!state.inES5() && vars.ecmaIdentifiers[5][t.id] === false) ||
+                  (!state.inESNext() && vars.ecmaIdentifiers[6][t.id] === false)) {
                 warning("W129", t.token, t.id);
               }
             }
@@ -3634,7 +3634,7 @@ var JSHINT = (function() {
   function classdef(isStatement) {
 
     /*jshint validthis:true */
-    if (!state.option.inESNext()) {
+    if (!state.inESNext()) {
       warning("W104", state.tokens.curr, "class");
     }
     if (isStatement) {
@@ -3653,21 +3653,19 @@ var JSHINT = (function() {
   }
 
   function classtail(c) {
-    var strictness = state.directive["use strict"];
-
+    var wasInClassBody = state.inClassBody;
     // ClassHeritage(opt)
     if (state.tokens.next.value === "extends") {
       advance("extends");
       c.heritage = expression(10);
     }
 
-    // A ClassBody is always strict code.
-    state.directive["use strict"] = true;
+    state.inClassBody = true;
     advance("{");
     // ClassBody(opt)
     c.body = classbody(c);
     advance("}");
-    state.directive["use strict"] = strictness;
+    state.inClassBody = wasInClassBody;
   }
 
   function classbody(c) {
@@ -3775,7 +3773,7 @@ var JSHINT = (function() {
     var generator = false;
     if (state.tokens.next.value === "*") {
       advance("*");
-      if (state.option.inESNext(true)) {
+      if (state.inESNext({ strict: true })) {
         generator = true;
       } else {
         warning("W119", state.tokens.curr, "function*");
@@ -3813,7 +3811,7 @@ var JSHINT = (function() {
     var generator = false;
 
     if (state.tokens.next.value === "*") {
-      if (!state.option.inESNext()) {
+      if (!state.inESNext()) {
         warning("W119", state.tokens.curr, "function*");
       }
       advance("*");
@@ -3914,7 +3912,7 @@ var JSHINT = (function() {
       }
 
       if (state.tokens.next.value === "if") {
-        if (!state.option.inMoz(true)) {
+        if (!state.inMoz()) {
           warning("W118", state.tokens.curr, "catch filter");
         }
         advance("if");
@@ -3939,7 +3937,7 @@ var JSHINT = (function() {
 
     while (state.tokens.next.id === "catch") {
       increaseComplexityCount();
-      if (b && (!state.option.inMoz(true))) {
+      if (b && (!state.inMoz())) {
         warning("W118", state.tokens.next, "multiple catch blocks");
       }
       doCatch();
@@ -3975,7 +3973,7 @@ var JSHINT = (function() {
 
   blockstmt("with", function() {
     var t = state.tokens.next;
-    if (state.directive["use strict"]) {
+    if (state.isStrict()) {
       error("E010", state.tokens.curr);
     } else if (!state.option.withstmt) {
       warning("W085", state.tokens.curr);
@@ -4135,7 +4133,7 @@ var JSHINT = (function() {
     if (t.value === "each") {
       foreachtok = t;
       advance("each");
-      if (!state.option.inMoz(true)) {
+      if (!state.inMoz()) {
         warning("W118", state.tokens.curr, "for each");
       }
     }
@@ -4170,7 +4168,7 @@ var JSHINT = (function() {
 
     // if we're in a for (… in|of …) statement
     if (_.contains(inof, nextop.value)) {
-      if (!state.option.inESNext() && nextop.value === "of") {
+      if (!state.inESNext() && nextop.value === "of") {
         error("W104", nextop, "for of");
       }
 
@@ -4378,12 +4376,12 @@ var JSHINT = (function() {
     x.lbp = 25;
   }(prefix("yield", function() {
     var prev = state.tokens.prev;
-    if (state.option.inESNext(true) && !funct["(generator)"]) {
+    if (state.inESNext(true) && !funct["(generator)"]) {
       // If it's a yield within a catch clause inside a generator then that's ok
       if (!("(catch)" === funct["(name)"] && funct["(context)"]["(generator)"])) {
         error("E046", state.tokens.curr, "yield");
       }
-    } else if (!state.option.inESNext()) {
+    } else if (!state.inESNext()) {
       warning("W104", state.tokens.curr, "yield");
     }
     funct["(generator)"] = "yielded";
@@ -4394,7 +4392,7 @@ var JSHINT = (function() {
       advance("*");
     }
 
-    if (this.line === startLine(state.tokens.next) || !state.option.inMoz(true)) {
+    if (this.line === startLine(state.tokens.next) || !state.inMoz()) {
       if (delegatingYield ||
           (state.tokens.next.id !== ";" && !state.tokens.next.reach && state.tokens.next.nud)) {
 
@@ -4407,7 +4405,7 @@ var JSHINT = (function() {
         }
       }
 
-      if (state.option.inMoz(true) && state.tokens.next.id !== ")" &&
+      if (state.inMoz() && state.tokens.next.id !== ")" &&
           (prev.lbp > 30 || (!prev.assign && !isEndOfExpr()) || prev.id === "yield")) {
         error("E050", this);
       }
@@ -4428,7 +4426,7 @@ var JSHINT = (function() {
   }).exps = true;
 
   stmt("import", function() {
-    if (!state.option.inESNext()) {
+    if (!state.inESNext()) {
       warning("W119", state.tokens.curr, "import");
     }
 
@@ -4509,7 +4507,7 @@ var JSHINT = (function() {
     var token;
     var identifier;
 
-    if (!state.option.inESNext()) {
+    if (!state.inESNext()) {
       warning("W119", state.tokens.curr, "export");
       ok = false;
     }
@@ -4782,7 +4780,7 @@ var JSHINT = (function() {
 
     var block = lookupBlockType();
     if (block.notJson) {
-      if (!state.option.inESNext() && block.isDestAssign) {
+      if (!state.inESNext() && block.isDestAssign) {
         warning("W104", state.tokens.curr, "destructuring assignment");
       }
       statements();
@@ -5344,9 +5342,10 @@ var JSHINT = (function() {
       default:
         directives();
 
-        if (state.directive["use strict"]) {
+        if (state.isStrict()) {
           if (!state.option.globalstrict) {
-            if (!(state.option.node || state.option.phantom || state.option.browserify)) {
+            if (!(state.option.module || state.option.node || state.option.phantom ||
+              state.option.browserify)) {
               warning("W097", state.tokens.prev);
             }
           }
diff --git a/src/lex.js b/src/lex.js
index 260b7e74..814b63fc 100644
--- a/src/lex.js
+++ b/src/lex.js
@@ -962,7 +962,7 @@ Lexer.prototype = {
         line: this.line,
         character: this.char
       }, checks,
-      function() { return n >= 0 && n <= 7 && state.directive["use strict"]; });
+      function() { return n >= 0 && n <= 7 && state.isStrict(); });
       break;
     case "u":
       var hexCode = this.input.substr(1, 4);
@@ -1566,7 +1566,7 @@ Lexer.prototype = {
       }
       var meta = token.meta;
 
-      if (meta && meta.isFutureReservedWord && state.option.inES5()) {
+      if (meta && meta.isFutureReservedWord && state.inES5()) {
         // ES3 FutureReservedWord in an ES5 environment.
         if (!meta.es5) {
           return false;
@@ -1575,7 +1575,7 @@ Lexer.prototype = {
         // Some ES5 FutureReservedWord identifiers are active only
         // within a strict mode environment.
         if (meta.strictOnly) {
-          if (!state.option.strict && !state.directive["use strict"]) {
+          if (!state.option.strict && !state.isStrict()) {
             return false;
           }
         }
@@ -1785,7 +1785,7 @@ Lexer.prototype = {
           line: this.line,
           character: this.char
         }, checks, function() {
-          return state.directive["use strict"] && token.base === 8 && token.isLegacy;
+          return state.isStrict() && token.base === 8 && token.isLegacy;
         });
 
         this.trigger("Number", {
diff --git a/src/messages.js b/src/messages.js
index 1df2fb7b..1f33d1bb 100644
--- a/src/messages.js
+++ b/src/messages.js
@@ -69,7 +69,8 @@ var errors = {
   E051: "Regular parameters cannot come after default parameters.",
   E052: "Unclosed template literal.",
   E053: "Export declaration must be in global scope.",
-  E054: "Class properties must be methods. Expected '(' but instead saw '{a}'."
+  E054: "Class properties must be methods. Expected '(' but instead saw '{a}'.",
+  E055: "The '{a}' option cannot be set after any executable code."
 };
 
 var warnings = {
@@ -208,7 +209,8 @@ var warnings = {
   W130: "Invalid element after rest element.",
   W131: "Invalid parameter after rest parameter.",
   W132: "`var` declarations are forbidden. Use `let` or `const` instead.",
-  W133: "Invalid for-{a} loop left-hand-side: {b}."
+  W133: "Invalid for-{a} loop left-hand-side: {b}.",
+  W134: "The '{a}' option is only available when linting ECMAScript {b} code."
 };
 
 var info = {
diff --git a/src/options.js b/src/options.js
index 93496c10..3775a5aa 100644
--- a/src/options.js
+++ b/src/options.js
@@ -641,6 +641,12 @@ exports.bool = {
      */
     mocha       : true,
 
+    /**
+     * This option informs JSHint that the input code describes an ECMAScript 6
+     * module. All module code is interpreted as strict mode code.
+     */
+    module      : true,
+
     /**
      * This option defines globals available when your code is running as a
      * script for the [Windows Script
diff --git a/src/state.js b/src/state.js
index 463704fc..cef3ee40 100644
--- a/src/state.js
+++ b/src/state.js
@@ -4,6 +4,42 @@ var NameStack = require("./name-stack.js");
 var state = {
   syntax: {},
 
+  /**
+   * Determine if the code currently being linted is strict mode code.
+   *
+   * @returns {boolean}
+   */
+  isStrict: function() {
+    return this.directive["use strict"] || this.inClassBody ||
+      this.option.module;
+  },
+
+  // Assumption: chronologically ES3 < ES5 < ES6/ESNext < Moz
+  inMoz: function() {
+    return this.option.moz && !this.option.esnext;
+  },
+
+  /**
+   * @param {object} options
+   * @param {boolean} options.strict - When `true`, only consider ESNext when
+   *                                   in "esnext" code and *not* in "moz".
+   */
+  inESNext: function(strict) {
+    if (strict) {
+      return !this.option.moz && this.option.esnext;
+    }
+    return this.option.moz || this.option.esnext;
+  },
+
+  inES5: function() {
+    return !this.option.es3;
+  },
+
+  inES3: function() {
+    return this.option.es3;
+  },
+
+
   reset: function() {
     this.tokens = {
       prev: null,
@@ -22,6 +58,7 @@ var state = {
     this.ignoredLines = {};
     this.forinifcheckneeded = false;
     this.nameStack = new NameStack();
+    this.inClassBody = false;
 
     // Blank out non-multi-line-commented lines when ignoring linter errors
     this.ignoreLinterErrors = false;


Test patch:
diff --git a/tests/unit/options.js b/tests/unit/options.js
index 6462f5da..cf44df92 100644
--- a/tests/unit/options.js
+++ b/tests/unit/options.js
@@ -2397,3 +2397,116 @@ exports.errorI003 = function(test) {
 
   test.done();
 };
+
+exports.module = {};
+exports.module.behavior = function(test) {
+  var code = [
+    "var package = 3;",
+    "function f() { return this; }"
+  ];
+
+  TestRun(test)
+    .test(code, {});
+
+  TestRun(test)
+    .addError(0, "The 'module' option is only available when linting ECMAScript 6 code.")
+    .addError(1, "Expected an identifier and instead saw 'package' (a reserved word).")
+    .addError(2, "Possible strict violation.")
+    .test(code, { module: true });
+
+  TestRun(test)
+    .addError(1, "Expected an identifier and instead saw 'package' (a reserved word).")
+    .addError(2, "Possible strict violation.")
+    .test(code, { module: true, esnext: true });
+
+  code = [
+    "/* jshint module: true */",
+    "var package = 3;",
+    "function f() { return this; }"
+  ];
+
+  TestRun(test)
+    .addError(1, "The 'module' option is only available when linting ECMAScript 6 code.")
+    .addError(2, "Expected an identifier and instead saw 'package' (a reserved word).")
+    .addError(3, "Possible strict violation.")
+    .test(code);
+
+  code[0] = "/* jshint module: true, esnext: true */";
+
+  TestRun(test)
+    .addError(2, "Expected an identifier and instead saw 'package' (a reserved word).")
+    .addError(3, "Possible strict violation.")
+    .test(code);
+
+  test.done();
+};
+
+exports.module.declarationRestrictions = function( test ) {
+  TestRun(test)
+    .addError(2, "The 'module' option cannot be set after any executable code.")
+    .test([
+      "(function() {",
+      "  /* jshint module: true */",
+      "})();"
+    ], { esnext: true });
+
+  TestRun(test)
+    .addError(2, "The 'module' option cannot be set after any executable code.")
+    .test([
+      "void 0;",
+      "/* jshint module: true */"
+    ], { esnext: true });
+
+  TestRun(test)
+    .addError(3, "The 'module' option cannot be set after any executable code.")
+    .test([
+      "void 0;",
+      "// hide",
+      "/* jshint module: true */"
+    ], { esnext: true });
+
+  TestRun(test, "First line (following statement)")
+    .addError(1, "The 'module' option cannot be set after any executable code.")
+    .test([
+      "(function() {})(); /* jshint module: true */"
+    ], { esnext: true });
+
+  TestRun(test, "First line (within statement)")
+    .addError(1, "The 'module' option cannot be set after any executable code.")
+    .test([
+      "(function() { /* jshint module: true */",
+      "})();"
+    ], { esnext: true });
+
+  TestRun(test, "First line (before statement)")
+    .test([
+      "/* jshint module: true */ (function() {",
+      "})();"
+    ], { esnext: true });
+
+  TestRun(test, "First line (within expression)")
+    .addError(1, "The 'module' option cannot be set after any executable code.")
+    .test("Math.abs(/*jshint module: true */4);", { esnext: true });
+
+  TestRun(test, "Following single-line comment")
+    .test([
+      "// License boilerplate",
+      "/* jshint module: true */"
+    ], { esnext: true });
+
+  TestRun(test, "Following multi-line comment")
+    .test([
+      "/**",
+      " * License boilerplate",
+      " */",
+      "  /* jshint module: true */"
+    ], { esnext: true });
+
+  TestRun(test, "Following shebang")
+    .test([
+      "#!/usr/bin/env node",
+      "/* jshint module: true */"
+    ], { esnext: true });
+
+  test.done();
+};