Skip to content

Commit

Permalink
Merge df32e65 into 4ad9cb5
Browse files Browse the repository at this point in the history
  • Loading branch information
benmosher committed Feb 23, 2016
2 parents 4ad9cb5 + df32e65 commit 9d07d57
Show file tree
Hide file tree
Showing 15 changed files with 496 additions and 97 deletions.
2 changes: 0 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ reports/
resolvers/

# config
.babelrc
.eslintrc.yml
.travis.yml
appveyor.yml
.coveralls.yml
.editorconfig
.gitignore
gulpfile.js

# project stuff
*.sublime-*
Expand Down
12 changes: 12 additions & 0 deletions config/stage-0.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Rules in progress.
*
* Do not expect these to adhere to semver across releases.
* @type {Object}
*/
module.exports = {
plugins: ['import'],
rules: {
'import/no-deprecated': 1,
}
}
40 changes: 40 additions & 0 deletions docs/rules/no-deprecated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# no-deprecated

**Stage: 0**

**NOTE**: this rule is currently a work in progress. There may be "breaking" changes: most likely, additional cases that are flagged.

Reports use of a deprecated name, as indicated by a JSDoc block with a `@deprecated`
tag, i.e.

```js
// @file: ./answer.js

/**
* this is what you get when you trust a mouse talk show
* @deprecated need to restart the experiment
* @returns {Number} nonsense
*/
export function multiply(six, nine) {
return 42
}
```

will report as such:

```js
import { multiply } from './answer' // Deprecated: need to restart the experiment

function whatever(y, z) {
return multiply(y, z) // Deprecated: need to restart the experiment
}
```

### Worklist

- [x] report explicit imports on the import node
- [ ] support namespaces
- [ ] should bubble up through deep namespaces (#157)
- [x] report explicit imports at reference time (at the identifier) similar to namespace
- [x] mark module deprecated if file JSDoc has a @deprecated tag?

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"scripts": {
"watch": "NODE_PATH=./lib gulp watch-test",
"cover": "gulp pretest && NODE_PATH=./lib istanbul cover --dir reports/coverage _mocha tests/lib/ -- --recursive -R progress",
"posttest": "eslint ./src",
"test": "NODE_PATH=./lib gulp test",
"ci-test": "eslint ./src && gulp pretest && istanbul cover --report lcovonly --dir reports/coverage _mocha tests/lib/ -- --recursive --reporter dot",
"debug": "NODE_PATH=./lib mocha debug --recursive --reporter dot tests/lib/",
"prepublish": "eslint ./src && gulp prepublish",
"prepublish": "gulp prepublish",
"coveralls": "cat ./reports/coverage/lcov.info | coveralls"
},
"repository": {
Expand Down Expand Up @@ -61,6 +62,7 @@
},
"dependencies": {
"babel-runtime": "6.5.0",
"doctrine": "1.2.0",
"eslint-import-resolver-node": "^0.1.0"
},
"greenkeeper": {
Expand Down
11 changes: 11 additions & 0 deletions src/core/declaredScope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function declaredScope(context, name) {
let references = context.getScope().references
, i
for (i = 0; i < references.length; i++) {
if (references[i].identifier.name === name) {
break
}
}
if (!references[i]) return undefined
return references[i].resolved.scope.type
}
147 changes: 84 additions & 63 deletions src/core/getExports.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs'

import { createHash } from 'crypto'
import * as doctrine from 'doctrine'

import parse from './parse'
import resolve from './resolve'
Expand All @@ -12,7 +13,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 @@ -80,10 +81,63 @@ export default class ExportMap {
return m // can't continue
}

// attempt to collect module doc
ast.comments.some(c => {
if (c.type !== 'Block') return false
try {
const doc = doctrine.parse(c.value, { unwrap: true })
if (doc.tags.some(t => t.title === 'module')) {
m.doc = doc
return true
}
} catch (err) { /* ignore */ }
return false
})


ast.body.forEach(function (n) {
m.captureDefault(n)
m.captureAll(n, path)
m.captureNamedDeclaration(n, path)

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

if (n.type === 'ExportAllDeclaration') {
let remoteMap = m.resolveReExport(n, path)
if (remoteMap == null) return
remoteMap.named.forEach((value, name) => { m.named.set(name, value) })
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))
break
case 'VariableDeclaration':
n.declaration.declarations.forEach((d) =>
recursivePatternCapture(d.id, id => m.named.set(id.name, captureMetadata(d, n))))
break
}
}

// capture specifiers
let remoteMap
if (n.source) remoteMap = m.resolveReExport(n, path)

n.specifiers.forEach(function (s) {
if (s.type === 'ExportDefaultSpecifier') {
// don't add it if it is not present in the exported module
if (!remoteMap || !remoteMap.hasDefault) return
}
m.named.set(s.exported.name, null)
})
}

})

return m
Expand All @@ -96,65 +150,6 @@ export default class ExportMap {
return ExportMap.for(remotePath, this.context)
}

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

/**
* capture all named exports from remote module.
*
* returns null if this node wasn't an ExportAllDeclaration
* returns false if it was not resolved
* returns true if it was resolved + parsed
*
* @param {node} n
* @param {string} path - the path of the module currently parsing
* @return {boolean?}
*/
captureAll(n, path) {
if (n.type !== 'ExportAllDeclaration') return null

var remoteMap = this.resolveReExport(n, path)
if (remoteMap == null) return false

remoteMap.named.forEach(function (name) { this.named.add(name) }.bind(this))

return true
}

captureNamedDeclaration(n, path) {
if (n.type !== 'ExportNamedDeclaration') return

// capture declaration
if (n.declaration != null) {
switch (n.declaration.type) {
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'TypeAlias': // flowtype with babel-eslint parser
this.named.add(n.declaration.id.name)
break
case 'VariableDeclaration':
n.declaration.declarations.forEach((d) =>
recursivePatternCapture(d.id, id => this.named.add(id.name)))
break
}
}

// capture specifiers
let remoteMap
if (n.source) remoteMap = this.resolveReExport(n, path)

n.specifiers.forEach(function (s) {
if (s.type === 'ExportDefaultSpecifier') {
// don't add it if it is not present in the exported module
if (!remoteMap || !remoteMap.hasDefault) return
}

this.named.add(s.exported.name)
}.bind(this))
}

reportErrors(context, declaration) {
context.report({
node: declaration.source,
Expand All @@ -166,6 +161,32 @@ export default class ExportMap {
}
}

/**
* parse JSDoc from the first node that has leading comments
* @param {...[type]} nodes [description]
* @return {[type]} [description]
*/
function captureMetadata(...nodes) {
const metadata = {}

// 'some' short-circuits on first 'true'
nodes.some(n => {
if (!n.leadingComments) return false

// capture XSDoc
n.leadingComments.forEach(comment => {
// skip non-block comments
if (comment.value.slice(0, 4) !== "*\n *") return
try {
metadata.doc = doctrine.parse(comment.value, { unwrap: true })
} catch (err) {
/* don't care, for now? maybe add to `errors?` */
}
})
return true
})
return metadata
}

/**
* Traverse a patter/identifier node, calling 'callback'
Expand Down
3 changes: 3 additions & 0 deletions src/core/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export default function (p, context) {
parserOptions = Object.assign({}, parserOptions)
parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures)

// always attach comments
parserOptions.attachComment = true

// require the parser relative to the main module (i.e., ESLint)
const parser = requireParser(parserPath)

Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const rules = {
'no-amd': require('./rules/no-amd'),
'no-duplicates': require('./rules/no-duplicates'),
'imports-first': require('./rules/imports-first'),

// metadata-based
'no-deprecated': require('./rules/no-deprecated'),
}

export const configs = {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,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.keys()) {
addNamed(name, node)
}
},
Expand Down
15 changes: 2 additions & 13 deletions src/rules/namespace.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Exports from '../core/getExports'
import importDeclaration from '../importDeclaration'
import declaredScope from '../core/declaredScope'

module.exports = function (context) {

Expand Down Expand Up @@ -30,18 +31,6 @@ module.exports = function (context) {
namespace.name + '.'
}

function declaredScope(name) {
let references = context.getScope().references
, i
for (i = 0; i < references.length; i++) {
if (references[i].identifier.name === name) {
break
}
}
if (!references[i]) return undefined
return references[i].resolved.scope.type
}

return {
'ImportNamespaceSpecifier': function (namespace) {
const imports = getImportsAndReport(namespace)
Expand Down Expand Up @@ -88,7 +77,7 @@ module.exports = function (context) {
if (!namespaces.has(init.name)) return

// check for redefinition in intermediate scopes
if (declaredScope(init.name) !== 'module') return
if (declaredScope(context, init.name) !== 'module') return

const namespace = namespaces.get(init.name)

Expand Down
Loading

0 comments on commit 9d07d57

Please sign in to comment.