Skip to content

Commit

Permalink
Merge pull request #157 from benmosher/deep-namespace
Browse files Browse the repository at this point in the history
Deep namespaces!
  • Loading branch information
benmosher committed Feb 24, 2016
2 parents 78b9647 + b76908f commit 7fc99ee
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 148 deletions.
47 changes: 45 additions & 2 deletions docs/rules/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 29 additions & 9 deletions src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default class ExportMap {
return m // can't continue
}


// attempt to collect module doc
ast.comments.some(c => {
if (c.type !== 'Block') return false
Expand All @@ -94,11 +95,12 @@ export default class ExportMap {
return false
})

const namespaces = new Map()

ast.body.forEach(function (n) {

if (n.type === 'ExportDefaultDeclaration') {
m.named.set('default', captureMetadata(n))
m.named.set('default', captureDoc(n))
return
}

Expand All @@ -109,18 +111,27 @@ export default class ExportMap {
return
}

// 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)
}
return
}

if (n.type === 'ExportNamedDeclaration'){
// capture declaration
if (n.declaration != null) {
switch (n.declaration.type) {
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'TypeAlias': // flowtype with babel-eslint parser
m.named.set(n.declaration.id.name, captureMetadata(n))
m.named.set(n.declaration.id.name, captureDoc(n))
break
case 'VariableDeclaration':
n.declaration.declarations.forEach((d) =>
recursivePatternCapture(d.id, id => m.named.set(id.name, captureMetadata(d, n))))
recursivePatternCapture(d.id, id => m.named.set(id.name, captureDoc(d, n))))
break
}
}
Expand All @@ -129,15 +140,23 @@ export default class ExportMap {
let remoteMap
if (n.source) remoteMap = m.resolveReExport(n, path)

n.specifiers.forEach(function (s) {
n.specifiers.forEach((s) => {
const exportMeta = {}

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 = m.resolveReExport(namespaces.get(s.local.name), path)
if (namespace) exportMeta.namespace = namespace.named
} else if (s.type === 'ExportNamespaceSpecifier') {
exportMeta.namespace = remoteMap.named
}
m.named.set(s.exported.name, null)

// todo: JSDoc
m.named.set(s.exported.name, exportMeta)
})
}

})

return m
Expand All @@ -164,9 +183,9 @@ export default class ExportMap {
/**
* parse JSDoc from the first node that has leading comments
* @param {...[type]} nodes [description]
* @return {[type]} [description]
* @return {{doc: object}}
*/
function captureMetadata(...nodes) {
function captureDoc(...nodes) {
const metadata = {}

// 'some' short-circuits on first 'true'
Expand All @@ -185,11 +204,12 @@ function captureMetadata(...nodes) {
})
return true
})

return metadata
}

/**
* Traverse a patter/identifier node, calling 'callback'
* Traverse a pattern/identifier node, calling 'callback'
* for each leaf identifier.
* @param {node} pattern
* @param {Function} callback
Expand Down
87 changes: 58 additions & 29 deletions src/rules/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ module.exports = function (context) {
return imports
}

function message(identifier, namespace) {
return '\'' + identifier.name +
'\' not found in imported namespace ' +
namespace.name + '.'
function makeMessage(last, namepath) {
return `'${last.name}' not found in` +
(namepath.length > 1 ? ' deeply ' : ' ') +
`imported namespace '${namepath.join('.')}'.`
}

return {
Expand All @@ -55,45 +55,74 @@ 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,
makeMessage(dereference.property, namepath))
break
}

// stash and pop
namepath.push(dereference.property.name)
namespace = namespace.get(dereference.property.name).namespace
dereference = dereference.parent
}

},

'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(context, 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),
})
// 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: makeMessage(property.key, path),
})
continue
}

path.push(property.key.name)
testKey(property.value, namespace.get(property.key.name).namespace, path)
path.pop()
}
}

testKey(id, namespaces.get(init.name))
},
}
}
1 change: 1 addition & 0 deletions tests/files/deep-es7/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as b from './b'
1 change: 1 addition & 0 deletions tests/files/deep-es7/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as c from './c'
1 change: 1 addition & 0 deletions tests/files/deep-es7/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as d from './d'
1 change: 1 addition & 0 deletions tests/files/deep-es7/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const e = "e"
2 changes: 2 additions & 0 deletions tests/files/deep/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as b from './b'
export { b }
2 changes: 2 additions & 0 deletions tests/files/deep/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as c from './c'
export { c }
2 changes: 2 additions & 0 deletions tests/files/deep/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as d from './d'
export { d }
1 change: 1 addition & 0 deletions tests/files/deep/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const e = "e"
19 changes: 19 additions & 0 deletions tests/src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})

})
Loading

0 comments on commit 7fc99ee

Please sign in to comment.