Skip to content

Commit

Permalink
actual deep namespace checking
Browse files Browse the repository at this point in the history
first steps toward #119: collecting deep namespaces, and failing tests
  • Loading branch information
Ben Mosher authored and benmosher committed Feb 24, 2016
1 parent ff0ef21 commit 21f4ef8
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 30 deletions.
35 changes: 24 additions & 11 deletions src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
}
Expand Down Expand Up @@ -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
Expand All @@ -94,7 +96,7 @@ export default class ExportMap {

captureDefault(n) {
if (n.type !== 'ExportDefaultDeclaration') return
this.named.add('default')
this.named.set('default', null)
}

/**
Expand All @@ -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
Expand All @@ -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
}
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/rules/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
},
Expand Down
47 changes: 30 additions & 17 deletions src/rules/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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 }) {
Expand All @@ -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),
})
}
}
Expand Down
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"
21 changes: 20 additions & 1 deletion tests/src/rules/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.` }
}


Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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'." ],
}),
],
})

0 comments on commit 21f4ef8

Please sign in to comment.