From 21f4ef8b0bd15a6e83aa8ecafaca68f44cc5ec3b Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 21 Jan 2016 07:35:08 -0500 Subject: [PATCH 1/8] actual deep namespace checking first steps toward #119: collecting deep namespaces, and failing tests --- src/core/getExports.js | 35 ++++++++++++++++++--------- src/rules/export.js | 2 +- src/rules/namespace.js | 47 +++++++++++++++++++++++------------- tests/files/deep/a.js | 2 ++ tests/files/deep/b.js | 2 ++ tests/files/deep/c.js | 2 ++ tests/files/deep/d.js | 1 + tests/src/rules/namespace.js | 21 +++++++++++++++- 8 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 tests/files/deep/a.js create mode 100644 tests/files/deep/b.js create mode 100644 tests/files/deep/c.js create mode 100644 tests/files/deep/d.js diff --git a/src/core/getExports.js b/src/core/getExports.js index cc63647e5e..e6004479b0 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -12,7 +12,7 @@ const exportCaches = new Map() export default class ExportMap { constructor(context) { this.context = context - this.named = new Set() + this.named = new Map() this.errors = [] } @@ -76,10 +76,12 @@ export default class ExportMap { return m // can't continue } + const namespaces = new Map() + ast.body.forEach(function (n) { m.captureDefault(n) m.captureAll(n, path) - m.captureNamedDeclaration(n, path) + m.captureNamedDeclaration(n, path, namespaces) }) return m @@ -94,7 +96,7 @@ export default class ExportMap { captureDefault(n) { if (n.type !== 'ExportDefaultDeclaration') return - this.named.add('default') + this.named.set('default', null) } /** @@ -114,12 +116,19 @@ export default class ExportMap { var remoteMap = this.resolveReExport(n, path) if (remoteMap == null) return false - remoteMap.named.forEach(function (name) { this.named.add(name) }.bind(this)) + remoteMap.named.forEach((val, name) => { this.named.set(name, val) }) return true } - captureNamedDeclaration(n, path) { + captureNamedDeclaration(n, path, namespaces) { + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + let ns + if (n.specifiers.some(s => s.type === 'ImportNamespaceSpecifier' && (ns = s))) { + namespaces.set(ns.local.name, n) + } + } if (n.type !== 'ExportNamedDeclaration') return // capture declaration @@ -128,11 +137,11 @@ export default class ExportMap { case 'FunctionDeclaration': case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser - this.named.add(n.declaration.id.name) + this.named.set(n.declaration.id.name, null) // todo: capture type info break case 'VariableDeclaration': n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, id => this.named.add(id.name))) + recursivePatternCapture(d.id, id => this.named.set(id.name, null))) break } } @@ -141,20 +150,24 @@ export default class ExportMap { let remoteMap if (n.source) remoteMap = this.resolveReExport(n, path) - n.specifiers.forEach(function (s) { + n.specifiers.forEach((s) => { + let type = null if (s.type === 'ExportDefaultSpecifier') { // don't add it if it is not present in the exported module if (!remoteMap || !remoteMap.hasDefault) return + } else if (s.type === 'ExportSpecifier' && namespaces.has(s.local.name)){ + let namespace = this.resolveReExport(namespaces.get(s.local.name), path) + if (namespace) type = namespace.named } - this.named.add(s.exported.name) - }.bind(this)) + this.named.set(s.exported.name, type) + }) } } /** - * Traverse a patter/identifier node, calling 'callback' + * Traverse a pattern/identifier node, calling 'callback' * for each leaf identifier. * @param {node} pattern * @param {Function} callback diff --git a/src/rules/export.js b/src/rules/export.js index 5dc531b3f3..05d07ffa7d 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -58,7 +58,7 @@ module.exports = function (context) { `No named exports found in module '${node.source.value}'.`) } - for (let name of remoteExports.named) { + for (let [name] of remoteExports.named) { addNamed(name, node) } }, diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 98632ed899..e55e108842 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -14,8 +14,7 @@ module.exports = function (context) { if (imports.errors.length) { context.report({ node: declaration.source, - message: `Parse errors in imported module ` + - `'${declaration.source.value}'.`, + message: `Parse errors in imported module '${declaration.source.value}'.`, }) return } @@ -29,9 +28,7 @@ module.exports = function (context) { } function message(identifier, namespace) { - return '\'' + identifier.name + - '\' not found in imported namespace ' + - namespace.name + '.' + return `'${identifier.name}' not found in imported namespace ${namespace}.` } function declaredScope(name) { @@ -70,19 +67,35 @@ module.exports = function (context) { `Assignment to member of namespace '${dereference.object.name}'.`) } - if (dereference.computed) { - context.report(dereference.property, - 'Unable to validate computed reference to imported namespace \'' + - dereference.object.name + '\'.') - return - } - + // go deep var namespace = namespaces.get(dereference.object.name) - if (!namespace.has(dereference.property.name)) { - context.report( dereference.property - , message(dereference.property, dereference.object) - ) + var namepath = [dereference.object.name] + // while property is namespace and parent is member expression, keep validating + while (namespace instanceof Map && + dereference.type === 'MemberExpression') { + + if (dereference.computed) { + context.report(dereference.property, + 'Unable to validate computed reference to imported namespace \'' + + dereference.object.name + '\'.') + return + } + + if (!namespace.has(dereference.property.name)) { + context.report( + dereference.property, + `'${dereference.property.name}' not found in` + + (namepath.length > 1 ? ' deeply ' : ' ') + + `imported namespace '${namepath.join('.')}'.`) + break + } + + // stash and pop + namepath.push(dereference.property.name) + namespace = namespace.get(dereference.property.name) + dereference = dereference.parent } + }, 'VariableDeclarator': function ({ id, init }) { @@ -105,7 +118,7 @@ module.exports = function (context) { } else if (!namespace.has(property.key.name)) { context.report({ node: property, - message: message(property.key, init), + message: message(property.key, init.name), }) } } diff --git a/tests/files/deep/a.js b/tests/files/deep/a.js new file mode 100644 index 0000000000..655f0d64e9 --- /dev/null +++ b/tests/files/deep/a.js @@ -0,0 +1,2 @@ +import * as b from './b' +export { b } \ No newline at end of file diff --git a/tests/files/deep/b.js b/tests/files/deep/b.js new file mode 100644 index 0000000000..0aa1bba581 --- /dev/null +++ b/tests/files/deep/b.js @@ -0,0 +1,2 @@ +import * as c from './c' +export { c } \ No newline at end of file diff --git a/tests/files/deep/c.js b/tests/files/deep/c.js new file mode 100644 index 0000000000..37dd476361 --- /dev/null +++ b/tests/files/deep/c.js @@ -0,0 +1,2 @@ +import * as d from './d' +export { d } \ No newline at end of file diff --git a/tests/files/deep/d.js b/tests/files/deep/d.js new file mode 100644 index 0000000000..6a539d9422 --- /dev/null +++ b/tests/files/deep/d.js @@ -0,0 +1 @@ +export const e = "e" \ No newline at end of file diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index d1a5fb0d9d..2f1018d2f8 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -6,7 +6,7 @@ var ruleTester = new RuleTester({ env: { es6: true }}) function error(name, namespace) { - return { message: `'${name}' not found in imported namespace ${namespace}.` } + return { message: `'${name}' not found in imported namespace '${namespace}'.` } } @@ -57,6 +57,12 @@ ruleTester.run('namespace', rule, { // non-existent is handled by no-unresolved test({ code: 'export * as names from "./does-not-exist"' , parser: 'babel-eslint' }), + + /////////////////////// + // deep dereferences // + /////////////////////// + + test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e)' }), ], invalid: [ @@ -112,5 +118,18 @@ ruleTester.run('namespace', rule, { type: 'Literal', }], }), + + + /////////////////////// + // deep dereferences // + /////////////////////// + test({ + code: 'import * as a from "./deep/a"; console.log(a.b.e)', + errors: [ "'e' not found in deeply imported namespace 'a.b'." ], + }), + test({ + code: 'import * as a from "./deep/a"; console.log(a.b.c.e)', + errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], + }), ], }) From 36a03d38987c45e290684a614112882be29f1d36 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 26 Jan 2016 05:06:18 -0500 Subject: [PATCH 2/8] deep destructuring validation --- src/rules/namespace.js | 52 ++++++++++++++++++++++++------------ tests/src/rules/namespace.js | 9 +++++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/rules/namespace.js b/src/rules/namespace.js index e55e108842..15d8d2ab1e 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -31,6 +31,12 @@ module.exports = function (context) { return `'${identifier.name}' not found in imported namespace ${namespace}.` } + function deepMessage(last, namepath) { + return `'${last.name}' not found in` + + (namepath.length > 1 ? ' deeply ' : ' ') + + `imported namespace '${namepath.join('.')}'.` + } + function declaredScope(name) { let references = context.getScope().references , i @@ -84,9 +90,7 @@ module.exports = function (context) { if (!namespace.has(dereference.property.name)) { context.report( dereference.property, - `'${dereference.property.name}' not found in` + - (namepath.length > 1 ? ' deeply ' : ' ') + - `imported namespace '${namepath.join('.')}'.`) + deepMessage(dereference.property, namepath)) break } @@ -100,28 +104,42 @@ module.exports = function (context) { 'VariableDeclarator': function ({ id, init }) { if (init == null) return - if (id.type !== 'ObjectPattern') return if (init.type !== 'Identifier') return if (!namespaces.has(init.name)) return // check for redefinition in intermediate scopes if (declaredScope(init.name) !== 'module') return - const namespace = namespaces.get(init.name) - - for (let property of id.properties) { - if (property.key.type !== 'Identifier') { - context.report({ - node: property, - message: 'Only destructure top-level names.', - }) - } else if (!namespace.has(property.key.name)) { - context.report({ - node: property, - message: message(property.key, init.name), - }) + // DFS traverse child namespaces + function testKey(pattern, namespace, path = [init.name]) { + if (!(namespace instanceof Map)) return + if (pattern.type !== 'ObjectPattern') return + + for (let property of pattern.properties) { + + if (property.key.type !== 'Identifier') { + context.report({ + node: property, + message: 'Only destructure top-level names.', + }) + continue + } + + if (!namespace.has(property.key.name)) { + context.report({ + node: property, + message: deepMessage(property.key, path), + }) + continue + } + + path.push(property.key.name) + testKey(property.value, namespace.get(property.key.name), path) + path.pop() } } + + testKey(id, namespaces.get(init.name)) }, } } diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 2f1018d2f8..a865ff80c2 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -63,6 +63,7 @@ ruleTester.run('namespace', rule, { /////////////////////// test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e)' }), + test({ code: 'import * as a from "./deep/a"; var {b:{c:{d:{e}}}} = a' }), ], invalid: [ @@ -131,5 +132,13 @@ ruleTester.run('namespace', rule, { code: 'import * as a from "./deep/a"; console.log(a.b.c.e)', errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], }), + test({ + code: 'import * as a from "./deep/a"; var {b:{ e }} = a', + errors: [ "'e' not found in deeply imported namespace 'a.b'." ], + }), + test({ + code: 'import * as a from "./deep/a"; var {b:{c:{ e }}} = a', + errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], + }), ], }) From 687f27d4e23324e09f71d61fbfd1520dd9279b28 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 26 Jan 2016 07:08:05 -0500 Subject: [PATCH 3/8] replace vestigial 'message' function with ~`deepMessage`~ `makeMessage` --- src/rules/namespace.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 15d8d2ab1e..c6e5e97b91 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -27,11 +27,7 @@ module.exports = function (context) { return imports } - function message(identifier, namespace) { - return `'${identifier.name}' not found in imported namespace ${namespace}.` - } - - function deepMessage(last, namepath) { + function makeMessage(last, namepath) { return `'${last.name}' not found in` + (namepath.length > 1 ? ' deeply ' : ' ') + `imported namespace '${namepath.join('.')}'.` @@ -90,7 +86,7 @@ module.exports = function (context) { if (!namespace.has(dereference.property.name)) { context.report( dereference.property, - deepMessage(dereference.property, namepath)) + makeMessage(dereference.property, namepath)) break } @@ -128,7 +124,7 @@ module.exports = function (context) { if (!namespace.has(property.key.name)) { context.report({ node: property, - message: deepMessage(property.key, path), + message: makeMessage(property.key, path), }) continue } From 9b7564eb7545ce9d89bfe757fbc6a10771247e86 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 26 Jan 2016 17:21:56 -0500 Subject: [PATCH 4/8] referencing children of children is fine --- tests/src/rules/namespace.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index a865ff80c2..bff7daec10 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -63,6 +63,7 @@ ruleTester.run('namespace', rule, { /////////////////////// test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e)' }), + test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e.f)' }), test({ code: 'import * as a from "./deep/a"; var {b:{c:{d:{e}}}} = a' }), ], From 8bd05e3a77472d3348619ee680696622b1fae3aa Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 6 Feb 2016 15:42:23 -0500 Subject: [PATCH 5/8] grep tests using TEST_GREP variable value --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 8b4299b01f..a861494d5e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -78,7 +78,7 @@ gulp.task('pretest', ['src', 'tests', 'wipe-extras']) gulp.task('test', ['pretest'], function () { return gulp.src('tests/lib/**/*.js', { read: false }) - .pipe(mocha({ reporter: 'dot' })) + .pipe(mocha({ reporter: 'dot', grep: process.env.TEST_GREP })) // NODE_PATH=./lib mocha --recursive --reporter dot tests/lib/ }) From 7f987c59af7c49b2c8d81ddf3385cab50fd7bfd0 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 6 Feb 2016 15:42:48 -0500 Subject: [PATCH 6/8] WIP ES7 namespace export support --- src/rules/namespace.js | 2 +- tests/files/deep-es7/a.js | 1 + tests/files/deep-es7/b.js | 1 + tests/files/deep-es7/c.js | 1 + tests/files/deep-es7/d.js | 1 + tests/src/rules/namespace.js | 247 ++++++++++++++++++----------------- 6 files changed, 130 insertions(+), 123 deletions(-) create mode 100644 tests/files/deep-es7/a.js create mode 100644 tests/files/deep-es7/b.js create mode 100644 tests/files/deep-es7/c.js create mode 100644 tests/files/deep-es7/d.js diff --git a/src/rules/namespace.js b/src/rules/namespace.js index c6e5e97b91..8427468ba4 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -14,7 +14,7 @@ module.exports = function (context) { if (imports.errors.length) { context.report({ node: declaration.source, - message: `Parse errors in imported module '${declaration.source.value}'.`, + message: `Parse errors in imported module '${declaration.source.value}'.` + JSON.stringify(imports.errors), }) return } diff --git a/tests/files/deep-es7/a.js b/tests/files/deep-es7/a.js new file mode 100644 index 0000000000..baafcdd313 --- /dev/null +++ b/tests/files/deep-es7/a.js @@ -0,0 +1 @@ +export * as b from './b' diff --git a/tests/files/deep-es7/b.js b/tests/files/deep-es7/b.js new file mode 100644 index 0000000000..bd0fb92a67 --- /dev/null +++ b/tests/files/deep-es7/b.js @@ -0,0 +1 @@ +export * as c from './c' diff --git a/tests/files/deep-es7/c.js b/tests/files/deep-es7/c.js new file mode 100644 index 0000000000..8533356605 --- /dev/null +++ b/tests/files/deep-es7/c.js @@ -0,0 +1 @@ +export * as d from './d' diff --git a/tests/files/deep-es7/d.js b/tests/files/deep-es7/d.js new file mode 100644 index 0000000000..6a539d9422 --- /dev/null +++ b/tests/files/deep-es7/d.js @@ -0,0 +1 @@ +export const e = "e" \ No newline at end of file diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index bff7daec10..a3a2483848 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -9,137 +9,140 @@ function error(name, namespace) { return { message: `'${name}' not found in imported namespace '${namespace}'.` } } - -ruleTester.run('namespace', rule, { - valid: [ - test({ code: "import * as foo from './empty-folder';"}), - test({ code: 'import * as names from "./named-exports"; ' + - 'console.log((names.b).c); ' }), - - test({ code: 'import * as names from "./named-exports"; ' + - 'console.log(names.a);' }), - test({ code: 'import * as names from "./re-export-names"; ' + - 'console.log(names.foo);' }), - test({ code: "import * as elements from './jsx';" - , settings: { 'import/parse-options': { plugins: ['jsx'] }}}), - test({ code: "import * as foo from './common';" - , settings: { 'import/ignore': ['common'] } }), - - // destructuring namespaces - test({ code: 'import * as names from "./named-exports";' + - 'const { a } = names' }), - test({ code: 'import * as names from "./named-exports";' + - 'const { d: c } = names' }), - test({ code: 'import * as names from "./named-exports";' + - 'const { c } = foo\n' + - ' , { length } = "names"\n' + - ' , alt = names' }), - // deep destructuring only cares about top level - test({ code: 'import * as names from "./named-exports";' + - 'const { ExportedClass: { length } } = names' }), - - // detect scope redefinition - test({ code: 'import * as names from "./named-exports";' + - 'function b(names) { const { c } = names }' }), - test({ code: 'import * as names from "./named-exports";' + - 'function b() { let names = null; const { c } = names }' }), - test({ code: 'import * as names from "./named-exports";' + - 'const x = function names() { const { c } = names }' }), - - - ///////// - // es7 // - ///////// - test({ code: 'export * as names from "./named-exports"' - , parser: 'babel-eslint' }), - test({ code: 'export defport, * as names from "./named-exports"' - , parser: 'babel-eslint' }), - // non-existent is handled by no-unresolved - test({ code: 'export * as names from "./does-not-exist"' - , parser: 'babel-eslint' }), - - /////////////////////// - // deep dereferences // - /////////////////////// - - test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e)' }), - test({ code: 'import * as a from "./deep/a"; console.log(a.b.c.d.e.f)' }), - test({ code: 'import * as a from "./deep/a"; var {b:{c:{d:{e}}}} = a' }), - ], - - invalid: [ - test({code: "import * as foo from './common';", - errors: ["No exported names found in module './common'."]}), - test({code: "import * as names from './default-export';", - errors: ["No exported names found in module './default-export'."]}), - - test({ code: "import * as names from './named-exports'; " + - ' console.log(names.c);' - , errors: [error('c', 'names')] }), - - test({ code: "import * as names from './named-exports';" + - " console.log(names['a']);" - , errors: 1 }), - - // assignment warning (from no-reassign) - test({ code: 'import * as foo from \'./bar\'; foo.foo = \'y\';' - , errors: [{ message: 'Assignment to member of namespace \'foo\'.'}] }), - test({ code: 'import * as foo from \'./bar\'; foo.x = \'y\';' - , errors: 2 }), - - // invalid destructuring - test({ code: 'import * as names from "./named-exports";' + - 'const { c } = names' - , errors: [{ type: 'Property' }] }), - test({ code: 'import * as names from "./named-exports";' + - 'function b() { const { c } = names }' - , errors: [{ type: 'Property' }] }), - test({ code: 'import * as names from "./named-exports";' + - 'const { c: d } = names' - , errors: [{ type: 'Property' }] }), - test({ code: 'import * as names from "./named-exports";' + - 'const { c: { d } } = names' - , errors: [{ type: 'Property' }] }), - - ///////// - // es7 // - ///////// - test({ code: 'export * as names from "./default-export"' - , parser: 'babel-eslint' - , errors: 1 }), - test({ code: 'export defport, * as names from "./default-export"' - , parser: 'babel-eslint' - , errors: 1 }), - - - // parse errors +const valid = [ + test({ code: "import * as foo from './empty-folder';"}), + test({ code: 'import * as names from "./named-exports"; ' + + 'console.log((names.b).c); ' }), + + test({ code: 'import * as names from "./named-exports"; ' + + 'console.log(names.a);' }), + test({ code: 'import * as names from "./re-export-names"; ' + + 'console.log(names.foo);' }), + test({ code: "import * as elements from './jsx';" + , settings: { 'import/parse-options': { plugins: ['jsx'] }}}), + test({ code: "import * as foo from './common';" + , settings: { 'import/ignore': ['common'] } }), + + // destructuring namespaces + test({ code: 'import * as names from "./named-exports";' + + 'const { a } = names' }), + test({ code: 'import * as names from "./named-exports";' + + 'const { d: c } = names' }), + test({ code: 'import * as names from "./named-exports";' + + 'const { c } = foo\n' + + ' , { length } = "names"\n' + + ' , alt = names' }), + // deep destructuring only cares about top level + test({ code: 'import * as names from "./named-exports";' + + 'const { ExportedClass: { length } } = names' }), + + // detect scope redefinition + test({ code: 'import * as names from "./named-exports";' + + 'function b(names) { const { c } = names }' }), + test({ code: 'import * as names from "./named-exports";' + + 'function b() { let names = null; const { c } = names }' }), + test({ code: 'import * as names from "./named-exports";' + + 'const x = function names() { const { c } = names }' }), + + + ///////// + // es7 // + ///////// + test({ code: 'export * as names from "./named-exports"' + , parser: 'babel-eslint' }), + test({ code: 'export defport, * as names from "./named-exports"' + , parser: 'babel-eslint' }), + // non-existent is handled by no-unresolved + test({ code: 'export * as names from "./does-not-exist"' + , parser: 'babel-eslint' }), +] + +const invalid = [ + test({code: "import * as foo from './common';", + errors: ["No exported names found in module './common'."]}), + test({code: "import * as names from './default-export';", + errors: ["No exported names found in module './default-export'."]}), + + test({ code: "import * as names from './named-exports'; " + + ' console.log(names.c);' + , errors: [error('c', 'names')] }), + + test({ code: "import * as names from './named-exports';" + + " console.log(names['a']);" + , errors: 1 }), + + // assignment warning (from no-reassign) + test({ code: 'import * as foo from \'./bar\'; foo.foo = \'y\';' + , errors: [{ message: 'Assignment to member of namespace \'foo\'.'}] }), + test({ code: 'import * as foo from \'./bar\'; foo.x = \'y\';' + , errors: 2 }), + + // invalid destructuring + test({ code: 'import * as names from "./named-exports";' + + 'const { c } = names' + , errors: [{ type: 'Property' }] }), + test({ code: 'import * as names from "./named-exports";' + + 'function b() { const { c } = names }' + , errors: [{ type: 'Property' }] }), + test({ code: 'import * as names from "./named-exports";' + + 'const { c: d } = names' + , errors: [{ type: 'Property' }] }), + test({ code: 'import * as names from "./named-exports";' + + 'const { c: { d } } = names' + , errors: [{ type: 'Property' }] }), + + ///////// + // es7 // + ///////// + test({ code: 'export * as names from "./default-export"' + , parser: 'babel-eslint' + , errors: 1 }), + test({ code: 'export defport, * as names from "./default-export"' + , parser: 'babel-eslint' + , errors: 1 }), + + + // parse errors + test({ + code: "import * as namespace from './malformed.js';", + errors: [{ + message: "Parse errors in imported module './malformed.js'.", + type: 'Literal', + }], + }), + +] + +/////////////////////// +// deep dereferences // +////////////////////// +;[['deep', 'espree'], ['deep-es7', 'babel-eslint']].forEach(function ([folder, parser]) { // close over params + valid.push( + test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.d.e)` }), + test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.d.e.f)` }), + test({ parser, code: `import * as a from "./${folder}/a"; var {b:{c:{d:{e}}}} = a` })) + + invalid.push( test({ - code: "import * as namespace from './malformed.js';", - errors: [{ - message: "Parse errors in imported module './malformed.js'.", - type: 'Literal', - }], - }), - - - /////////////////////// - // deep dereferences // - /////////////////////// - test({ - code: 'import * as a from "./deep/a"; console.log(a.b.e)', + parser, + code: `import * as a from "./${folder}/a"; console.log(a.b.e)`, errors: [ "'e' not found in deeply imported namespace 'a.b'." ], }), test({ - code: 'import * as a from "./deep/a"; console.log(a.b.c.e)', + parser, + code: `import * as a from "./${folder}/a"; console.log(a.b.c.e)`, errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], }), test({ - code: 'import * as a from "./deep/a"; var {b:{ e }} = a', + parser, + code: `import * as a from "./${folder}/a"; var {b:{ e }} = a`, errors: [ "'e' not found in deeply imported namespace 'a.b'." ], }), test({ - code: 'import * as a from "./deep/a"; var {b:{c:{ e }}} = a', + parser, + code: `import * as a from "./${folder}/a"; var {b:{c:{ e }}} = a`, errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], - }), - ], + })) }) + +ruleTester.run('namespace', rule, { valid, invalid }) From a55c9746bf251040e95fa282d133126d85efc300 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Wed, 24 Feb 2016 07:14:33 -0500 Subject: [PATCH 7/8] added support for deep crawls over ES7 exported namespaces --- src/core/getExports.js | 2 ++ tests/src/core/getExports.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/core/getExports.js b/src/core/getExports.js index 20cec4201e..6564fee764 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -149,6 +149,8 @@ export default class ExportMap { } else if (s.type === 'ExportSpecifier' && namespaces.has(s.local.name)){ let namespace = m.resolveReExport(namespaces.get(s.local.name), path) if (namespace) exportMeta.namespace = namespace.named + } else if (s.type === 'ExportNamespaceSpecifier') { + exportMeta.namespace = remoteMap.named } // todo: JSDoc diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 9727f39a39..094415a39c 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -195,4 +195,23 @@ describe('getExports', function () { }) }) + context('exported static namespaces', function () { + const espreeContext = { parserPath: 'espree', parserOptions: { sourceType: 'module' }, settings: {} } + const babelContext = { parserPath: 'babel-eslint', parserOptions: { sourceType: 'module' }, settings: {} } + + it('works with espree & traditional namespace exports', function () { + const a = ExportMap.parse(getFilename('deep/a.js'), espreeContext) + expect(a.errors).to.be.empty + expect(a.named.get('b').namespace).to.exist + expect(a.named.get('b').namespace.has('c')).to.be.true + }) + + it('works with babel-eslint & ES7 namespace exports', function () { + const a = ExportMap.parse(getFilename('deep-es7/a.js'), babelContext) + expect(a.errors).to.be.empty + expect(a.named.get('b').namespace).to.exist + expect(a.named.get('b').namespace.has('c')).to.be.true + }) + }) + }) From b76908f9cca125edde5d5c909867ed8db4a9695b Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Wed, 24 Feb 2016 07:36:23 -0500 Subject: [PATCH 8/8] deep namespace docs --- docs/rules/namespace.md | 47 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/rules/namespace.md b/docs/rules/namespace.md index 5a9772fc73..d88457f94e 100644 --- a/docs/rules/namespace.md +++ b/docs/rules/namespace.md @@ -22,13 +22,56 @@ redefinition of the namespace in an intermediate scope. Adherence to the ESLint For [ES7], reports if an exported namespace would be empty (no names exported from the referenced module.) -TODO: examples. +Given: +```js +// @module ./named-exports +export const a = 1 +const b = 2 +export { b } + +const c = 3 +export { c as d } + +export class ExportedClass { } + +// ES7 +export * as deep from './deep' +``` +and: +```js +// @module ./deep +export const e = "MC2" +``` + +See what is valid and reported: + +```js +// @module ./foo +import * as names from './named-exports' + +function great() { + return names.a + names.b // so great https://youtu.be/ei7mb8UxEl8 +} + +function notGreat() { + doSomethingWith(names.c) // Reported: 'c' not found in imported namespace 'names'. + + const { a, b, c } = names // also reported, only for 'c' +} + +// also tunnels through re-exported namespaces! +function deepTrouble() { + doSomethingWith(names.deep.e) // fine + doSomethingWith(names.deep.f) // Reported: 'f' not found in deeply imported namespace 'names.deep'. +} + +``` ## Further Reading - Lee Byron's [ES7] export proposal - [`import/ignore`] setting -- [`jsnext:main`] (Rollup) +- [`jsnext:main`](Rollup) [ES7]: https://github.com/leebyron/ecmascript-more-export-from [`import/ignore`]: ../../README.md#importignore