Skip to content

Commit

Permalink
[[FIX]] ES6 modules respect undef and unused
Browse files Browse the repository at this point in the history
This adds scoping rules for ES6 modules, ensuring that undef and unused rules accurate reflect
the behavior of ES6 modules on the scope.

This also adds syntax parsing support for `export { foo as bar }` aliasing.

Fixes: #2147
Fixes: #2148
  • Loading branch information
leebyron committed Feb 4, 2015
1 parent 0ebe613 commit 438d928
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 21 deletions.
39 changes: 26 additions & 13 deletions src/jshint.js
Expand Up @@ -1394,9 +1394,8 @@ var JSHINT = (function() {
// fnparam means that this identifier is being defined as a function
// argument (see identifier())
// prop means that this identifier is that of an object property
// exported means that the identifier is part of a valid ES6 `export` declaration

function optionalidentifier(fnparam, prop, preserve, exported) {
function optionalidentifier(fnparam, prop, preserve) {
if (!state.tokens.next.identifier) {
return;
}
Expand All @@ -1408,10 +1407,6 @@ var JSHINT = (function() {
var curr = state.tokens.curr;
var val = state.tokens.curr.value;

if (exported) {
state.tokens.curr.exported = true;
}

if (!isReserved(curr)) {
return val;
}
Expand All @@ -1433,9 +1428,8 @@ var JSHINT = (function() {
// fnparam means that this identifier is being defined as a function
// argument
// prop means that this identifier is that of an object property
// `exported` means that the identifier token should be exported.
function identifier(fnparam, prop, exported) {
var i = optionalidentifier(fnparam, prop, false, exported);
function identifier(fnparam, prop) {
var i = optionalidentifier(fnparam, prop, false);
if (i) {
return i;
}
Expand Down Expand Up @@ -4340,11 +4334,22 @@ var JSHINT = (function() {
if (state.tokens.next.value === "{") {
// ExportDeclaration :: export ExportClause
advance("{");
var exportedTokens = [];
for (;;) {
var id;
exported[id = identifier(false, false, ok)] = ok;
if (ok) {
funct["(blockscope)"].setExported(id);
if (!state.tokens.next.identifier) {
error("E030", state.tokens.next, state.tokens.next.value);
}
advance();

state.tokens.curr.exported = ok;
exportedTokens.push(state.tokens.curr);

if (state.tokens.next.value === "as") {
advance("as");
if (!state.tokens.next.identifier) {
error("E030", state.tokens.next, state.tokens.next.value);
}
advance();
}

if (state.tokens.next.value === ",") {
Expand All @@ -4361,6 +4366,14 @@ var JSHINT = (function() {
// ExportDeclaration :: export ExportClause FromClause
advance("from");
advance("(string)");
} else if (ok) {
exportedTokens.forEach(function(token) {
if (!funct[token.value]) {
isundef(funct, "W117", token, token.value);
}
exported[token.value] = true;
funct["(blockscope)"].setExported(token.value);
});
}
return this;
}
Expand Down
87 changes: 79 additions & 8 deletions tests/unit/core.js
Expand Up @@ -685,15 +685,16 @@ exports.testES6Modules = function (test) {
.addError(26, "'export' is only available in ES6 (use esnext option).")
.addError(30, "'export' is only available in ES6 (use esnext option).")
.addError(31, "'export' is only available in ES6 (use esnext option).")
.addError(35, "'export' is only available in ES6 (use esnext option).")
.addError(39, "'export' is only available in ES6 (use esnext option).")
.addError(43, "'export' is only available in ES6 (use esnext option).")
.addError(45, "'export' is only available in ES6 (use esnext option).")
.addError(46, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.addError(47, "'export' is only available in ES6 (use esnext option).")
.addError(47, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.addError(32, "'export' is only available in ES6 (use esnext option).")
.addError(36, "'export' is only available in ES6 (use esnext option).")
.addError(40, "'export' is only available in ES6 (use esnext option).")
.addError(44, "'export' is only available in ES6 (use esnext option).")
.addError(46, "'export' is only available in ES6 (use esnext option).")
.addError(45, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.addError(47, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.addError(48, "'export' is only available in ES6 (use esnext option).")
.addError(48, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.addError(47, "'export' is only available in ES6 (use esnext option).")
.addError(46, "'class' is available in ES6 (use esnext option) or Mozilla JS extensions (use moz).")
.test(src, {});

var src2 = [
Expand Down Expand Up @@ -733,6 +734,76 @@ exports.testES6ModulesNamedExportsAffectUnused = function (test) {
test.done();
};

exports.testES6ModulesNamedExportsAffectUndef = function (test) {
// The identifier "foo" is expected to have been defined in the scope
// of this file in order to be exported.
// The example below is roughly similar to this Common JS:
//
// exports.foo = foo;
//
// Thus, the "foo" identifier should be seen as undefined.
var src1 = [
"export { foo };"
];

TestRun(test)
.addError(1, "'foo' is not defined.")
.test(src1, {
esnext: true,
undef: true
});

test.done();
};

exports.testES6ModulesThroughExportDoNotAffectUnused = function (test) {
// "Through" exports do not alter the scope of this file, but instead pass
// the exports from one source on through this source.
// The example below is roughly similar to this Common JS:
//
// var foo;
// exports.foo = require('source').foo;
//
// Thus, the "foo" identifier should be seen as unused.
var src1 = [
"var foo;",
"export { foo } from \"source\";"
];

TestRun(test)
.addError(1, "'foo' is defined but never used.")
.test(src1, {
esnext: true,
unused: true
});

test.done();
};

exports.testES6ModulesThroughExportDoNotAffectUndef = function (test) {
// "Through" exports do not alter the scope of this file, but instead pass
// the exports from one source on through this source.
// The example below is roughly similar to this Common JS:
//
// exports.foo = require('source').foo;
// var bar = foo;
//
// Thus, the "foo" identifier should be seen as undefined.
var src1 = [
"export { foo } from \"source\";",
"var bar = foo;"
];

TestRun(test)
.addError(2, "'foo' is not defined.")
.test(src1, {
esnext: true,
undef: true
});

test.done();
};

exports.testES6ModulesDefaultExportsAffectUnused = function (test) {
// Default Exports should count as used
var src1 = [
Expand Down
1 change: 1 addition & 0 deletions tests/unit/fixtures/es6-import-export.js
Expand Up @@ -29,6 +29,7 @@ export default function() {

export { foo };
export { foo, bar } from "source";
export { foo, bar as biz } from "source";

// gettin' fancy

Expand Down
2 changes: 2 additions & 0 deletions tests/unit/parser.js
Expand Up @@ -5104,6 +5104,8 @@ exports.testES6BlockExports = function (test) {
];

TestRun(test)
.addError(1, "'broken' is defined but never used.")
.addError(2, "'broken2' is defined but never used.")
.addError(4, "Export declaration must be in global scope.")
.addError(5, "Export declaration must be in global scope.")
.addError(6, "Export declaration must be in global scope.")
Expand Down

0 comments on commit 438d928

Please sign in to comment.